Compare commits
60 Commits
v1.0
...
f25feaa55a
| Author | SHA1 | Date | |
|---|---|---|---|
| f25feaa55a | |||
| 16639e2384 | |||
| 20ef495571 | |||
| 285a20f070 | |||
| baee5dba83 | |||
| 7d9c879a4e | |||
| 953e5e773c | |||
| 8a4bc1d1ab | |||
| c57c849073 | |||
| 6cc123f272 | |||
| 5168b23609 | |||
| c5299dd655 | |||
| 4bedd8c04b | |||
| 295b71c819 | |||
| 6629c8047f | |||
| bfa615a7f4 | |||
| 288d629f99 | |||
| 15fdbc7043 | |||
| f2f2348b54 | |||
| 9ba152b3c3 | |||
| 21a528d6d1 | |||
| 62ca3888d3 | |||
| 3ec56a925c | |||
| 95c08818cb | |||
| 42355bd1ef | |||
| fe2ce75cec | |||
| 98cac8a0a5 | |||
| 9ce46a0c7d | |||
| 266a43c09d | |||
| 8434301d1f | |||
| adfe3bf34e | |||
| 6a3c6db7a8 | |||
| 4ace3c9f27 | |||
| b52659df6c | |||
| 20877a3eb1 | |||
| 69875f27ad | |||
| 829f5e454e | |||
| 1c916a7fad | |||
| 88b24b5228 | |||
| ee8cbde296 | |||
| f638704fba | |||
| 4c63d5ebf3 | |||
| a3fb0d8018 | |||
| 88b16ca69e | |||
| ba3467810c | |||
| 0bfedd54cb | |||
| f25d7e4567 | |||
| 4cdedff654 | |||
| 4f99ab4164 | |||
| cdc5af7f45 | |||
| 20e5e58ded | |||
| 93a39e440a | |||
| ec1c554eb3 | |||
| d3b7ab8fbe | |||
| 5609594439 | |||
| da4149ec60 | |||
| aa30d13b91 | |||
| b854d295f9 | |||
| 26a24efeb8 | |||
| 496d77482d |
@@ -21,13 +21,13 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId = "com.lnkj.ln_jq_app"
|
applicationId = "com.lingniu.driver"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = 4
|
||||||
versionName = flutter.versionName
|
versionName = "1.2.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="小羚羚"
|
android:label="小羚羚"
|
||||||
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"
|
||||||
@@ -48,6 +50,64 @@
|
|||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
||||||
|
<!-- 请填写你自己的- appKey -->
|
||||||
|
<meta-data android:name="com.alibaba.app.appkey" android:value="335642645"/>
|
||||||
|
<!-- 请填写你自己的appSecret -->
|
||||||
|
<meta-data android:name="com.alibaba.app.appsecret" android:value="39628204345a4240b5b645b68a5896c7"/>
|
||||||
|
<!-- 华为通道的参数appid -->
|
||||||
|
<meta-data android:name="com.huawei.hms.client.appid" android:value="" />
|
||||||
|
|
||||||
|
<!-- vivo通道的参数api_key为appkey -->
|
||||||
|
<meta-data android:name="com.vivo.push.api_key" android:value="" />
|
||||||
|
<meta-data android:name="com.vivo.push.app_id" android:value="" />
|
||||||
|
|
||||||
|
<!-- honor通道的参数-->
|
||||||
|
<meta-data android:name="com.hihonor.push.app_id" android:value="" />
|
||||||
|
|
||||||
|
<!-- oppo -->
|
||||||
|
<meta-data android:name="com.oppo.push.key" android:value="" />
|
||||||
|
<meta-data android:name="com.oppo.push.secret" android:value="" />
|
||||||
|
|
||||||
|
<!-- 小米-->
|
||||||
|
<meta-data android:name="com.xiaomi.push.id" android:value="id=2222222222222222222" />
|
||||||
|
<meta-data android:name="com.xiaomi.push.key" android:value="id=5555555555555" />
|
||||||
|
|
||||||
|
<!-- 魅族-->
|
||||||
|
<meta-data android:name="com.meizu.push.id" android:value="" />
|
||||||
|
<meta-data android:name="com.meizu.push.key" android:value="" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 接收推送消息 -->
|
||||||
|
<receiver
|
||||||
|
android:name="com.aliyun.ams.push.AliyunPushMessageReceiver"
|
||||||
|
android:exported="false"> <!-- 为保证receiver安全,建议设置不可导出,如需对其他应用开放可通过android:permission进行限制 -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.alibaba.push2.action.NOTIFICATION_OPENED" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.alibaba.push2.action.NOTIFICATION_REMOVED" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.alibaba.sdk.android.push.RECEIVE" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<!-- 辅助弹窗Activity -->
|
||||||
|
<activity
|
||||||
|
android:name="com.aliyun.ams.push.PushPopupActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="${applicationId}"
|
||||||
|
android:path="/thirdpush"
|
||||||
|
android:scheme="agoo" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
|
|||||||
BIN
ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.jpg
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
@@ -3,6 +3,17 @@ allprojects {
|
|||||||
// 使用阿里云镜像
|
// 使用阿里云镜像
|
||||||
maven("https://maven.aliyun.com/repository/public")
|
maven("https://maven.aliyun.com/repository/public")
|
||||||
maven("https://maven.aliyun.com/repository/google")
|
maven("https://maven.aliyun.com/repository/google")
|
||||||
|
maven(
|
||||||
|
"https://maven.aliyun.com/nexus/content/repositories/releases/"
|
||||||
|
)
|
||||||
|
// 集成华为通道需要配置 HMS Core SDK 的 Maven地址
|
||||||
|
maven(
|
||||||
|
"https://developer.huawei.com/repo/"
|
||||||
|
)
|
||||||
|
|
||||||
|
maven(
|
||||||
|
"https://developer.hihonor.com/repo"
|
||||||
|
)
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
ln_jq_app/assets/html/car.png
Normal file
|
After Width: | Height: | Size: 6.6 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,63 +38,140 @@
|
|||||||
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: 'aa3a22c19ed76b27f8a587555d6981c8',
|
||||||
}
|
}
|
||||||
</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=ecd74ece8cb14c9dad67675f83c3274d&plugin=AMap.MoveAnimation,AMap.Driving,AMap.TruckDriving,AMap.AutoComplete,AMap.ToolBar,AMap.Scale,AMap.Geocoder">
|
||||||
|
</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="终点: 请输入目的地" />
|
||||||
<button onclick="startRouteSearch()">路径规划</button>
|
<button onclick="startRouteSearch()">路径规划</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="container"></div>
|
<div id="container"></div>
|
||||||
<div id="panel"></div>
|
|
||||||
|
|
||||||
<script>
|
<div id="panel"></div>
|
||||||
var map, marker, driving, truckDriving;
|
|
||||||
|
<!-- 自定义定位按钮 -->
|
||||||
|
<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>
|
||||||
|
var map, marker, driving, truckDriving, geocoder;
|
||||||
var currentLat, currentLng;
|
var currentLat, currentLng;
|
||||||
|
|
||||||
// 标记是否使用货车模式 (true: 货车, false: 轿车)
|
|
||||||
var isTruckMode = false;
|
var isTruckMode = false;
|
||||||
|
var isInitialLocationSet = false;
|
||||||
|
|
||||||
|
|
||||||
function initMap() {
|
function initMap() {
|
||||||
map = new AMap.Map('container', {
|
map = new AMap.Map('container', {
|
||||||
@@ -81,21 +180,43 @@
|
|||||||
viewMode: '3D'
|
viewMode: '3D'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- 2. 初始化 geocoder ---
|
||||||
|
geocoder = new AMap.Geocoder({
|
||||||
|
city: "全国" // 设置地理编码范围
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通知 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,107 +225,202 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 输入提示 (自动补全) ---
|
// --- 输入提示 ---
|
||||||
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({
|
||||||
map: map,
|
map: map,
|
||||||
position: position,
|
position: position,
|
||||||
icon: "https://webapi.amap.com/images/car.png",
|
icon: "car.png",
|
||||||
offset: new AMap.Pixel(-26, -13),
|
offset: new AMap.Pixel(-23.5, -15),
|
||||||
autoRotation: true,
|
autoRotation: true,
|
||||||
angle: 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. 逆地理编码并设置默认起点 ---
|
||||||
|
// 只有在第一次获取到位置时,才设置默认起点,避免覆盖用户手动输入的起点
|
||||||
|
if (!isInitialLocationSet) {
|
||||||
|
geocoder.getAddress(position, function (status, result) {
|
||||||
|
if (status === 'complete' && result.regeocode) {
|
||||||
|
let shortAddress = '';
|
||||||
|
const regeo = result.regeocode;
|
||||||
|
const addressComponent = regeo.addressComponent;
|
||||||
|
const pois = regeo.pois;
|
||||||
|
|
||||||
|
// 策略1: 优先使用最近的、类型合适的POI的名称
|
||||||
|
if (pois && pois.length > 0) {
|
||||||
|
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI,这类POI通常是具体的建筑或地点名
|
||||||
|
const significantPoi = pois.find(p => p.type.indexOf('商务住宅') === -
|
||||||
|
1 && p.type.indexOf('地名地址信息') === -1);
|
||||||
|
if (significantPoi) {
|
||||||
|
shortAddress = significantPoi.name;
|
||||||
|
} else {
|
||||||
|
// 如果找不到,就用第一个POI的名字
|
||||||
|
shortAddress = pois[0].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 策略2: 如果没有POI,使用"道路+门牌号"
|
||||||
|
else if (addressComponent.street && addressComponent.streetNumber) {
|
||||||
|
shortAddress = addressComponent.street + addressComponent
|
||||||
|
.streetNumber;
|
||||||
|
}
|
||||||
|
// 策略3: 如果还没有,使用"区+乡镇"
|
||||||
|
else if (addressComponent.district) {
|
||||||
|
shortAddress = addressComponent.district + (addressComponent
|
||||||
|
.township || '');
|
||||||
|
}
|
||||||
|
// 策略4: 降级到使用完整的、但可能很长的地址
|
||||||
|
else {
|
||||||
|
shortAddress = regeo.formattedAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果拼接出的地址过长,可以再做一次截断
|
||||||
|
if (shortAddress.length > 20) {
|
||||||
|
// 可以在这里添加更复杂的截断逻辑,比如按关键字
|
||||||
|
shortAddress = regeo.formattedAddress.substring(0, 20) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 将获取到的地址填充到起点输入框
|
||||||
|
document.getElementById('startInput').value = shortAddress;
|
||||||
|
isInitialLocationSet = true; // 标记为已设置,不再更新
|
||||||
|
} else {
|
||||||
|
// 如果逆地理编码失败,依然使用“当前位置”作为提示
|
||||||
|
document.getElementById('startInput').placeholder = "当前位置";
|
||||||
|
console.error('逆地理编码失败:', result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心功能 2: 点击按钮回到当前位置
|
||||||
|
*/
|
||||||
|
function 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("请输入终点");
|
||||||
// 清除之前的路线
|
|
||||||
if(driving) driving.clear();
|
|
||||||
if(truckDriving) truckDriving.clear();
|
|
||||||
|
|
||||||
// 构造起点和终点参数
|
|
||||||
// 高德API Search方法支持点对象数组:[{keyword: '名字', city: '城市'}, {keyword: '...'}]
|
|
||||||
var points = [];
|
|
||||||
|
|
||||||
// 1. 处理起点
|
|
||||||
if (startKw) {
|
|
||||||
// 如果用户输入了文字
|
|
||||||
points.push({ keyword: startKw });
|
|
||||||
} else {
|
|
||||||
// 如果用户没输入,使用当前定位
|
|
||||||
if (currentLng && currentLat) {
|
|
||||||
// 关键点:当混合使用坐标和关键字时,必须构建 LngLat 对象
|
|
||||||
points.push({
|
|
||||||
keyword: '我的位置',
|
|
||||||
location: new AMap.LngLat(currentLng, currentLat)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("FlutterLog: 无定位数据且无输入");
|
|
||||||
alert("未获取到定位,请输入起点");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 处理终点
|
|
||||||
points.push({ keyword: endKw });
|
|
||||||
|
|
||||||
// 显示底部面板
|
|
||||||
document.getElementById('panel').style.display = 'block';
|
|
||||||
|
|
||||||
// 3. 发起请求
|
|
||||||
if (isTruckMode && truckDriving) {
|
|
||||||
// 货车接口略有不同,需要传入 path 数组
|
|
||||||
// truckDriving.search(path, callback)
|
|
||||||
// 这里为了简化,我们先演示轿车。货车通常需要具体的经纬度,建议先通过 Geocoder 把 endKw 转成经纬度再传给货车接口
|
|
||||||
console.log("FlutterLog: 货车API需要更严格的经纬度参数,建议先使用轿车演示");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认使用轿车规划 (支持 keyword + location 混合)
|
|
||||||
driving.search(points, function(status, result) {
|
|
||||||
if (status === 'complete') {
|
|
||||||
console.log('FlutterLog: 规划成功');
|
|
||||||
} else {
|
|
||||||
console.log('FlutterLog: 规划失败: ' + result);
|
|
||||||
// 常见错误: "USER_DAILY_QUERY_OVER_LIMIT" (Key额度超限)
|
|
||||||
// "INVALID_USER_KEY" (Key错误)
|
|
||||||
// "no_data" (地点没找到)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 绘制自定义路径 (保持原有功能) ---
|
|
||||||
function drawCustomRoute(pointsJsonString) {
|
|
||||||
// 如果是从Flutter传来的JSON字符串,需要Parse,如果是对象则直接用
|
|
||||||
var path = typeof pointsJsonString === 'string' ? JSON.parse(pointsJsonString) : pointsJsonString;
|
|
||||||
|
|
||||||
|
// 清除旧路线
|
||||||
if (driving) driving.clear();
|
if (driving) driving.clear();
|
||||||
|
|
||||||
|
// 收起键盘
|
||||||
|
document.getElementById('startInput').blur();
|
||||||
|
document.getElementById('endInput').blur();
|
||||||
|
|
||||||
|
// --- 构造路径规划的点 (使用数组方式,更灵活) ---
|
||||||
|
var points = [];
|
||||||
|
|
||||||
|
// 1. 处理起点逻辑
|
||||||
|
// 如果输入框是空的,或者写着 "我的位置",则使用 GPS 坐标
|
||||||
|
if (!startKw || startKw === '我的位置') {
|
||||||
|
if (!currentLng || !currentLat) {
|
||||||
|
// 如果还没获取到定位
|
||||||
|
if (window.flutter_inappwebview) {
|
||||||
|
window.flutter_inappwebview.callHandler('requestLocation');
|
||||||
|
}
|
||||||
|
alert("正在获取定位,请稍后...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 使用精准坐标对象 (避免高德去猜 '我的位置' 关键词)
|
||||||
|
points.push({
|
||||||
|
keyword: '我的位置', // 用于显示的名字
|
||||||
|
location: new AMap.LngLat(currentLng, currentLat) // 实际导航用的坐标
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果用户手动输入了地点 (例如 "北京南站")
|
||||||
|
// 直接存入关键词,让高德自己去搜
|
||||||
|
points.push({
|
||||||
|
keyword: startKw
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 处理终点逻辑 (通常是关键词)
|
||||||
|
points.push({
|
||||||
|
keyword: endKw
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 发起搜索
|
||||||
|
// points 数组里现在是一个起点对象和一个终点对象
|
||||||
|
driving.search(points, function (status, result) {
|
||||||
|
if (status === 'complete') {
|
||||||
|
console.log('JS: 规划成功');
|
||||||
|
var panel = document.getElementById('panel');
|
||||||
|
panel.style.display = 'block';
|
||||||
|
document.body.classList.add('panel-active');
|
||||||
|
} else {
|
||||||
|
console.log('JS: 规划失败', result);
|
||||||
|
alert("规划失败,请检查起终点名称");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助功能:手动绘制路线 (Flutter 可能用到)
|
||||||
|
function drawCustomRoute(pointsJsonString) {
|
||||||
|
var path = typeof pointsJsonString === 'string' ? JSON.parse(pointsJsonString) : pointsJsonString;
|
||||||
|
if (driving) driving.clear();
|
||||||
var polyline = new AMap.Polyline({
|
var polyline = new AMap.Polyline({
|
||||||
path: path,
|
path: path,
|
||||||
isOutline: true,
|
isOutline: true,
|
||||||
@@ -219,6 +435,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.onload = initMap;
|
window.onload = initMap;
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
# platform :ios, '13.0'
|
platform :ios, '13.0'
|
||||||
|
|
||||||
|
source 'https://cdn.cocoapods.org/'
|
||||||
|
source 'https://gitee.com/aliyun/aliyun-specs.git'
|
||||||
|
source 'https://github.com/aliyun/aliyun-specs.git'
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
PODS:
|
PODS:
|
||||||
|
- AlicloudELS (1.0.3)
|
||||||
|
- AlicloudPush (3.2.3):
|
||||||
|
- AlicloudELS (= 1.0.3)
|
||||||
|
- AlicloudUTDID (~> 1.0)
|
||||||
|
- AlicloudUTDID (1.6.1)
|
||||||
|
- aliyun_push_flutter (0.0.1):
|
||||||
|
- AlicloudPush (< 4.0, >= 3.2.3)
|
||||||
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
@@ -20,7 +28,9 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- MTBBarcodeScanner (5.0.11)
|
- mobile_scanner (7.0.0):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- OrderedSet (6.0.3)
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -29,9 +39,6 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- qr_code_scanner_plus (0.2.6):
|
|
||||||
- Flutter
|
|
||||||
- MTBBarcodeScanner
|
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -39,6 +46,7 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- aliyun_push_flutter (from `.symlinks/plugins/aliyun_push_flutter/ios`)
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
@@ -47,19 +55,25 @@ DEPENDENCIES:
|
|||||||
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
|
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
|
||||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
|
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- qr_code_scanner_plus (from `.symlinks/plugins/qr_code_scanner_plus/ios`)
|
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
|
https://gitee.com/aliyun/aliyun-specs.git:
|
||||||
|
- AlicloudUTDID
|
||||||
|
https://github.com/aliyun/aliyun-specs.git:
|
||||||
|
- AlicloudELS
|
||||||
|
- AlicloudPush
|
||||||
trunk:
|
trunk:
|
||||||
- MTBBarcodeScanner
|
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
aliyun_push_flutter:
|
||||||
|
:path: ".symlinks/plugins/aliyun_push_flutter/ios"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
@@ -76,37 +90,40 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
|
mobile_scanner:
|
||||||
|
:path: ".symlinks/plugins/mobile_scanner/darwin"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
qr_code_scanner_plus:
|
|
||||||
:path: ".symlinks/plugins/qr_code_scanner_plus/ios"
|
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
AlicloudELS: fbf821383330465a5af84a033f36f263ae46ca41
|
||||||
|
AlicloudPush: 95150880af380f64cf1741f5586047c17d36c1d9
|
||||||
|
AlicloudUTDID: 5d2f22d50e11eecd38f30bc7a48c71925ea90976
|
||||||
|
aliyun_push_flutter: 0fc2f048a08687ef256c0cfdd72dd7a550ef3347
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
|
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_pdfview: 54e283d5851b0b247b3cc57877d35f1a05a204de
|
flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
qr_code_scanner_plus: 7e087021bc69873140e0754750eb87d867bed755
|
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
|
|
||||||
PODFILE CHECKSUM: 6416011b1bc721211379eaad259ff1cba3dbfad2
|
PODFILE CHECKSUM: 357c01ff4e7591871e8c4fd6462220a8c7447220
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
4B58A54CFC9A912F2BA04FF2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
4B58A54CFC9A912F2BA04FF2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
6AF04C5CFFF0B4098EEDA799 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
6AF04C5CFFF0B4098EEDA799 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
6D3F89E22F04C32900A154AD /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -143,6 +144,7 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */ = {
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
6D3F89E22F04C32900A154AD /* Runner.entitlements */,
|
||||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
@@ -487,7 +489,10 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = 2228B9MS38;
|
DEVELOPMENT_TEAM = 2228B9MS38;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -495,11 +500,89 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"-ObjC",
|
||||||
|
"-l\"swiftCoreGraphics\"",
|
||||||
|
"-framework",
|
||||||
|
"\"OrderedSet\"",
|
||||||
|
"-framework",
|
||||||
|
"\"connectivity_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"device_info_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_inappwebview_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_native_splash\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_pdfview\"",
|
||||||
|
"-framework",
|
||||||
|
"\"geolocator_apple\"",
|
||||||
|
"-framework",
|
||||||
|
"\"image_picker_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"mobile_scanner\"",
|
||||||
|
"-framework",
|
||||||
|
"\"package_info_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"path_provider_foundation\"",
|
||||||
|
"-framework",
|
||||||
|
"\"permission_handler_apple\"",
|
||||||
|
"-framework",
|
||||||
|
"\"shared_preferences_foundation\"",
|
||||||
|
"-framework",
|
||||||
|
"\"url_launcher_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"AlicloudELS\"",
|
||||||
|
"-framework",
|
||||||
|
"\"CloudPushSDK\"",
|
||||||
|
"-framework",
|
||||||
|
"\"aliyun_push_flutter\"",
|
||||||
|
);
|
||||||
|
"OTHER_LDFLAGS[arch=*]" = (
|
||||||
|
"$(inherited)",
|
||||||
|
"-ObjC",
|
||||||
|
"-l\"swiftCoreGraphics\"",
|
||||||
|
"-framework",
|
||||||
|
"\"OrderedSet\"",
|
||||||
|
"-framework",
|
||||||
|
"\"connectivity_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"device_info_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_inappwebview_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_native_splash\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_pdfview\"",
|
||||||
|
"-framework",
|
||||||
|
"\"geolocator_apple\"",
|
||||||
|
"-framework",
|
||||||
|
"\"image_picker_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"mobile_scanner\"",
|
||||||
|
"-framework",
|
||||||
|
"\"package_info_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"path_provider_foundation\"",
|
||||||
|
"-framework",
|
||||||
|
"\"permission_handler_apple\"",
|
||||||
|
"-framework",
|
||||||
|
"\"shared_preferences_foundation\"",
|
||||||
|
"-framework",
|
||||||
|
"\"url_launcher_ios\"",
|
||||||
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
@@ -674,7 +757,10 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = 2228B9MS38;
|
DEVELOPMENT_TEAM = 2228B9MS38;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -682,12 +768,90 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"-ObjC",
|
||||||
|
"-l\"swiftCoreGraphics\"",
|
||||||
|
"-framework",
|
||||||
|
"\"OrderedSet\"",
|
||||||
|
"-framework",
|
||||||
|
"\"connectivity_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"device_info_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_inappwebview_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_native_splash\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_pdfview\"",
|
||||||
|
"-framework",
|
||||||
|
"\"geolocator_apple\"",
|
||||||
|
"-framework",
|
||||||
|
"\"image_picker_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"mobile_scanner\"",
|
||||||
|
"-framework",
|
||||||
|
"\"package_info_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"path_provider_foundation\"",
|
||||||
|
"-framework",
|
||||||
|
"\"permission_handler_apple\"",
|
||||||
|
"-framework",
|
||||||
|
"\"shared_preferences_foundation\"",
|
||||||
|
"-framework",
|
||||||
|
"\"url_launcher_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"AlicloudELS\"",
|
||||||
|
"-framework",
|
||||||
|
"\"CloudPushSDK\"",
|
||||||
|
"-framework",
|
||||||
|
"\"aliyun_push_flutter\"",
|
||||||
|
);
|
||||||
|
"OTHER_LDFLAGS[arch=*]" = (
|
||||||
|
"$(inherited)",
|
||||||
|
"-ObjC",
|
||||||
|
"-l\"swiftCoreGraphics\"",
|
||||||
|
"-framework",
|
||||||
|
"\"OrderedSet\"",
|
||||||
|
"-framework",
|
||||||
|
"\"connectivity_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"device_info_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_inappwebview_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_native_splash\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_pdfview\"",
|
||||||
|
"-framework",
|
||||||
|
"\"geolocator_apple\"",
|
||||||
|
"-framework",
|
||||||
|
"\"image_picker_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"mobile_scanner\"",
|
||||||
|
"-framework",
|
||||||
|
"\"package_info_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"path_provider_foundation\"",
|
||||||
|
"-framework",
|
||||||
|
"\"permission_handler_apple\"",
|
||||||
|
"-framework",
|
||||||
|
"\"shared_preferences_foundation\"",
|
||||||
|
"-framework",
|
||||||
|
"\"url_launcher_ios\"",
|
||||||
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -698,7 +862,10 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = 2228B9MS38;
|
DEVELOPMENT_TEAM = 2228B9MS38;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -706,11 +873,56 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"-ObjC",
|
||||||
|
"-l\"swiftCoreGraphics\"",
|
||||||
|
"-framework",
|
||||||
|
"\"OrderedSet\"",
|
||||||
|
"-framework",
|
||||||
|
"\"connectivity_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"device_info_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_inappwebview_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_native_splash\"",
|
||||||
|
"-framework",
|
||||||
|
"\"flutter_pdfview\"",
|
||||||
|
"-framework",
|
||||||
|
"\"geolocator_apple\"",
|
||||||
|
"-framework",
|
||||||
|
"\"image_picker_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"mobile_scanner\"",
|
||||||
|
"-framework",
|
||||||
|
"\"package_info_plus\"",
|
||||||
|
"-framework",
|
||||||
|
"\"path_provider_foundation\"",
|
||||||
|
"-framework",
|
||||||
|
"\"permission_handler_apple\"",
|
||||||
|
"-framework",
|
||||||
|
"\"shared_preferences_foundation\"",
|
||||||
|
"-framework",
|
||||||
|
"\"url_launcher_ios\"",
|
||||||
|
"-framework",
|
||||||
|
"\"AlicloudELS\"",
|
||||||
|
"-framework",
|
||||||
|
"\"CloudPushSDK\"",
|
||||||
|
"-framework",
|
||||||
|
"\"aliyun_push_flutter\"",
|
||||||
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
|||||||
@@ -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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
"scale" : "3x",
|
||||||
"scale" : "2x"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
"filename" : "Icon-App-80x80.jpg",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"filename" : "Icon-App-40x40@3x.png",
|
"scale" : "2x",
|
||||||
"scale" : "3x"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "60x60",
|
"filename" : "Icon-App-120x120.jpg",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"filename" : "Icon-App-60x60@2x.png",
|
"scale" : "3x",
|
||||||
"scale" : "2x"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "60x60",
|
"filename" : "Icon-App-120x120 1.jpg",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"filename" : "Icon-App-60x60@3x.png",
|
"scale" : "2x",
|
||||||
"scale" : "3x"
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-180.jpg",
|
||||||
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 509 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 707 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 848 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -2,16 +2,10 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key></key>
|
||||||
<string>需要您的位置信息以在地图上展示</string>
|
<string></string>
|
||||||
<!-- 建议添加:即使你只申请WhenInUse,有些插件逻辑可能会检查这个key -->
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<true/>
|
||||||
<string>我们需要您的位置来规划路线</string>
|
|
||||||
<!-- 建议添加:旧版本iOS兼容 -->
|
|
||||||
<key>NSLocationAlwaysUsageDescription</key>
|
|
||||||
<string>我们需要您的位置来规划路线</string>
|
|
||||||
|
|
||||||
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@@ -32,8 +26,24 @@
|
|||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>需要访问您的相机以扫描二维码</string>
|
||||||
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
|
<string>我们需要您的位置来规划路线</string>
|
||||||
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
|
<string>我们需要您的位置来规划路线</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>需要您的位置信息以在地图上展示</string>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>需要访问您的相册以选择二维码图片进行识别</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>需要访问您的相册以选择二维码图片进行识别</string>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -51,17 +61,14 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>uses</key>
|
||||||
|
<string></string>
|
||||||
|
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>UIBackgroundModes</key>
|
||||||
<string>需要访问您的相机以扫描二维码</string>
|
<array>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<string>remote-notification</string>
|
||||||
<string>需要访问您的相册以选择二维码图片进行识别</string>
|
<string>fetch</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
</array>
|
||||||
<string>需要访问您的相册以选择二维码图片进行识别</string>
|
|
||||||
|
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
||||||
<true/>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
8
ln_jq_app/ios/Runner/Runner.entitlements
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -6,7 +6,6 @@ class BaseModel<T> {
|
|||||||
final String message; // 消息,例如 "success"
|
final String message; // 消息,例如 "success"
|
||||||
final String msg; // 消息,例如 "success"
|
final String msg; // 消息,例如 "success"
|
||||||
final T? data; // 核心数据,使用泛型 T,可以是任何类型
|
final T? data; // 核心数据,使用泛型 T,可以是任何类型
|
||||||
final int time; // 时间戳
|
|
||||||
final dynamic error; // 错误信息,可以是任何类型或 null
|
final dynamic error; // 错误信息,可以是任何类型或 null
|
||||||
|
|
||||||
BaseModel({
|
BaseModel({
|
||||||
@@ -15,7 +14,6 @@ class BaseModel<T> {
|
|||||||
required this.message,
|
required this.message,
|
||||||
required this.msg,
|
required this.msg,
|
||||||
this.data, // data 可以为 null
|
this.data, // data 可以为 null
|
||||||
required this.time,
|
|
||||||
this.error, // error 可以为 null
|
this.error, // error 可以为 null
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +58,6 @@ class BaseModel<T> {
|
|||||||
status: json['status'] as bool? ?? false,
|
status: json['status'] as bool? ?? false,
|
||||||
message: json['message'] ?? '暂不可用,请稍后',
|
message: json['message'] ?? '暂不可用,请稍后',
|
||||||
msg: json['msg'] ?? '暂不可用,请稍后',
|
msg: json['msg'] ?? '暂不可用,请稍后',
|
||||||
time: _parseInt(json['time']),
|
|
||||||
data: finalData,
|
data: finalData,
|
||||||
error: json['error'],
|
error: json['error'],
|
||||||
);
|
);
|
||||||
@@ -72,7 +69,6 @@ class BaseModel<T> {
|
|||||||
'status': status,
|
'status': status,
|
||||||
'message': message,
|
'message': message,
|
||||||
'msg': msg,
|
'msg': msg,
|
||||||
'time': time,
|
|
||||||
'data': data,
|
'data': data,
|
||||||
'error': error,
|
'error': error,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class VehicleInfo {
|
|||||||
final String engineNum;
|
final String engineNum;
|
||||||
final String truckNum;
|
final String truckNum;
|
||||||
final num hydrogenCapacity;
|
final num hydrogenCapacity;
|
||||||
final num maxHydrogen;
|
final String maxHydrogen;
|
||||||
|
|
||||||
VehicleInfo({
|
VehicleInfo({
|
||||||
required this.plateNumber,
|
required this.plateNumber,
|
||||||
@@ -36,7 +36,7 @@ class VehicleInfo {
|
|||||||
engineNum: json["engineNum"] ?? '',
|
engineNum: json["engineNum"] ?? '',
|
||||||
truckNum: json["truckNum"] ?? '',
|
truckNum: json["truckNum"] ?? '',
|
||||||
hydrogenCapacity: json["hydrogenCapacity"] ?? 0,
|
hydrogenCapacity: json["hydrogenCapacity"] ?? 0,
|
||||||
maxHydrogen: json["maxHydrogen"] ?? 0,
|
maxHydrogen: json["maxHydrogen"] ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
|||||||
@@ -7,11 +7,18 @@ class AppTheme {
|
|||||||
|
|
||||||
static const Color themeColor = Color(0xFF0c83c3);
|
static const Color themeColor = Color(0xFF0c83c3);
|
||||||
|
|
||||||
|
//是否开放域名切换
|
||||||
|
static const bool is_show_host = true;
|
||||||
|
|
||||||
static const String test_service_url = "http://47.100.49.118:8090/api/";
|
//http://192.168.110.222:8080/
|
||||||
|
//http://192.168.110.44:8080/
|
||||||
|
static String test_service_url = "https://beta-esg.api.lnh2e.com/";
|
||||||
static const String release_service_url = "";
|
static const String release_service_url = "";
|
||||||
|
|
||||||
//加氢站相关查询
|
//加氢站相关查询
|
||||||
static const String jiaqing_service_url = "https://lnh2e.com/api/lingniu-manager-v1/v1/";
|
static const String jiaqing_service_url =
|
||||||
|
"https://beta.lnh2e.com/api/lingniu-manager-v1/v1/";
|
||||||
|
|
||||||
//车辆信息
|
//车辆信息
|
||||||
static const String car_service_url = "http://47.99.166.38:20000/";
|
static const String car_service_url = "http://47.99.166.38:20000/";
|
||||||
|
|
||||||
@@ -19,6 +26,11 @@ class AppTheme {
|
|||||||
|
|
||||||
static const Color darkThemeColor = Color(0xFF032896);
|
static const Color darkThemeColor = Color(0xFF032896);
|
||||||
|
|
||||||
|
static const String android_key = "335642645";
|
||||||
|
static const String android_appsecret = "39628204345a4240b5b645b68a5896c7";
|
||||||
|
static const String ios_key = "335642649";
|
||||||
|
static const String ios_appsecret = "173bc08bd5df422da20c8e3ffbf0521b";
|
||||||
|
|
||||||
/// 亮色主题样式
|
/// 亮色主题样式
|
||||||
static ThemeData light = ThemeData(
|
static ThemeData light = ThemeData(
|
||||||
useMaterial3: false,
|
useMaterial3: false,
|
||||||
@@ -56,10 +68,7 @@ class AppTheme {
|
|||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: Color.fromARGB(255, 34, 34, 34),
|
backgroundColor: Color.fromARGB(255, 34, 34, 34),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
titleTextStyle: TextStyle(
|
titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
bottomAppBarTheme: BottomAppBarThemeData(
|
bottomAppBarTheme: BottomAppBarThemeData(
|
||||||
color: Color.fromARGB(255, 34, 34, 34),
|
color: Color.fromARGB(255, 34, 34, 34),
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
|
||||||
/// 专门用于处理和添加 Token 的拦截器
|
/// 专门用于处理和添加 Token 的拦截器
|
||||||
class TokenInterceptor extends Interceptor {
|
class TokenInterceptor extends Interceptor {
|
||||||
// 定义您想要使用的 Token Key
|
// 定义您想要使用的 Token Key
|
||||||
final String tokenKey;
|
final String tokenKey;
|
||||||
|
final String sourceKey;
|
||||||
|
|
||||||
// 构造函数,允许外部传入自定义的 Key,默认为 'Authorization'
|
// 构造函数,允许外部传入自定义的 Key,默认为 'Authorization'
|
||||||
TokenInterceptor({this.tokenKey = 'Authorization'});
|
TokenInterceptor({this.tokenKey = 'Authorization', this.sourceKey = 'source'});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async{
|
||||||
// 从 StorageService 中获取已保存的 token
|
// 从 StorageService 中获取已保存的 token
|
||||||
final String? token = StorageService.to.token;
|
final String? token = StorageService.to.token;
|
||||||
|
|
||||||
@@ -23,6 +27,21 @@ class TokenInterceptor extends Interceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String platformSource;
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
platformSource = 'Android';
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
platformSource = 'iOS';
|
||||||
|
} else {
|
||||||
|
platformSource = '';
|
||||||
|
}
|
||||||
|
if (!options.headers.containsKey(sourceKey)) {
|
||||||
|
options.headers[sourceKey] = platformSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.headers['appVersion'] = await getVersion();
|
||||||
|
options.headers['brand'] = await getDeviceModel();
|
||||||
|
|
||||||
// 调用 handler.next(options) 以继续执行请求
|
// 调用 handler.next(options) 以继续执行请求
|
||||||
// 这一步至关重要,否则请求会被中断
|
// 这一步至关重要,否则请求会被中断
|
||||||
super.onRequest(options, handler);
|
super.onRequest(options, handler);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:getx_scaffold/getx_scaffold.dart';
|
|||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
import 'package:ln_jq_app/common/token_interceptor.dart';
|
import 'package:ln_jq_app/common/token_interceptor.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
|
|
||||||
import 'common/styles/theme.dart';
|
import 'common/styles/theme.dart';
|
||||||
import 'pages/home/view.dart';
|
import 'pages/home/view.dart';
|
||||||
@@ -30,7 +31,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,
|
||||||
// 主题模式
|
// 主题模式
|
||||||
@@ -38,7 +39,7 @@ void main() async {
|
|||||||
// 主题
|
// 主题
|
||||||
theme: AppTheme.light,
|
theme: AppTheme.light,
|
||||||
// Dark主题
|
// Dark主题
|
||||||
darkTheme: AppTheme.dark,
|
darkTheme: AppTheme.light,
|
||||||
// AppTitle
|
// AppTitle
|
||||||
title: '小羚羚',
|
title: '小羚羚',
|
||||||
// 首页入口
|
// 首页入口
|
||||||
@@ -47,6 +48,8 @@ void main() async {
|
|||||||
fallbackLocale: Locale('zh', 'CN'),
|
fallbackLocale: Locale('zh', 'CN'),
|
||||||
supportedLocales: [Locale('zh', 'CN')],
|
supportedLocales: [Locale('zh', 'CN')],
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
|
//pull_to_refresh
|
||||||
|
RefreshLocalizations.delegate,
|
||||||
GlobalMaterialLocalizations.delegate,
|
GlobalMaterialLocalizations.delegate,
|
||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
@@ -62,6 +65,8 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void initHttpSet() {
|
void initHttpSet() {
|
||||||
|
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
|
||||||
|
|
||||||
// 设置基础 URL
|
// 设置基础 URL
|
||||||
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
||||||
//指定请求头
|
//指定请求头
|
||||||
@@ -69,15 +74,18 @@ void initHttpSet() {
|
|||||||
// 设置全局响应处理器
|
// 设置全局响应处理器
|
||||||
HttpService.to.setOnResponseHandler((response) async {
|
HttpService.to.setOnResponseHandler((response) async {
|
||||||
try {
|
try {
|
||||||
final baseModel = BaseModel<dynamic>.fromJson(response.data);
|
final baseModel = BaseModel.fromJson(response.data);
|
||||||
if (baseModel.code == 0 || baseModel.code == 200) {
|
if (baseModel.code == 0 || baseModel.code == 200) {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} else if (baseModel.code == 401) {
|
} else if (baseModel.code == 401) {
|
||||||
await StorageService.to.clearLoginInfo();
|
await StorageService.to.clearLoginInfo();
|
||||||
Get.offAll(() => LoginPage());
|
Get.offAll(() => LoginPage());
|
||||||
return baseModel.message;
|
return baseModel.message;
|
||||||
} else {
|
} else {
|
||||||
return baseModel.message;
|
return (baseModel.error.toString()).isEmpty
|
||||||
|
? "服务繁忙,稍后重试"
|
||||||
|
: baseModel.error.toString();
|
||||||
}
|
}
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
e.printInfo();
|
e.printInfo();
|
||||||
|
|||||||
@@ -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: '站点信息',
|
||||||
|
|||||||
216
ln_jq_app/lib/pages/b_page/history/controller.dart
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
|
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
|
||||||
|
|
||||||
|
class HistoryController extends GetxController {
|
||||||
|
// --- 定义 API 需要的日期格式化器 ---
|
||||||
|
final DateFormat _apiDateFormat = DateFormat('yyyy-MM-dd');
|
||||||
|
|
||||||
|
// 默认查询最近7天
|
||||||
|
final Rx<DateTime> startDate = DateTime.now().subtract(const Duration(days: 7)).obs;
|
||||||
|
final Rx<DateTime> endDate = DateTime.now().obs;
|
||||||
|
final TextEditingController plateNumberController = TextEditingController();
|
||||||
|
|
||||||
|
final RxString totalHydrogen = '0 kg'.obs;
|
||||||
|
final RxString totalCompletions = '0 次'.obs;
|
||||||
|
|
||||||
|
final RxList<ReservationModel> historyList = <ReservationModel>[].obs;
|
||||||
|
final RxBool isLoading = true.obs;
|
||||||
|
final RxBool hasData = false.obs;
|
||||||
|
|
||||||
|
String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value);
|
||||||
|
|
||||||
|
String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value);
|
||||||
|
String stationName = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
|
final args = Get.arguments as Map<String, dynamic>;
|
||||||
|
stationName = args['stationName'] as String;
|
||||||
|
fetchHistoryData();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> getAllOrderCounts() async {
|
||||||
|
var response = await HttpService.to.post(
|
||||||
|
"appointment/orderAddHyd/getAllOrderCounts",
|
||||||
|
data: {
|
||||||
|
// --- 直接使用 DateFormat 来格式化日期 ---
|
||||||
|
'startTime': _apiDateFormat.format(startDate.value),
|
||||||
|
'endTime': _apiDateFormat.format(endDate.value),
|
||||||
|
'plateNumber': plateNumberController.text,
|
||||||
|
'stationName': stationName, // 加氢站名称
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response == null || response.data == null) {
|
||||||
|
totalHydrogen.value = '0 kg';
|
||||||
|
totalCompletions.value = '0 次';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final baseModel = BaseModel<dynamic>.fromJson(response.data);
|
||||||
|
final dataMap = baseModel.data as Map<String, dynamic>;
|
||||||
|
totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0} kg';
|
||||||
|
totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0} 次';
|
||||||
|
} catch (e) {
|
||||||
|
totalHydrogen.value = '0 kg';
|
||||||
|
totalCompletions.value = '0 次';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchHistoryData() async {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
//获取数据
|
||||||
|
getAllOrderCounts();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await HttpService.to.post(
|
||||||
|
"appointment/orderAddHyd/sitOrderPage",
|
||||||
|
data: {
|
||||||
|
// --- 直接使用 DateFormat 来格式化日期 ---
|
||||||
|
'startTime': _apiDateFormat.format(startDate.value),
|
||||||
|
'endTime': _apiDateFormat.format(endDate.value),
|
||||||
|
'plateNumber': plateNumberController.text,
|
||||||
|
'pageNum': 1,
|
||||||
|
'pageSize': 50,
|
||||||
|
'stationName': stationName, // 加氢站名称
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null || response.data == null) {
|
||||||
|
showToast('无法获取历史记录');
|
||||||
|
_resetData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final baseModel = BaseModel<dynamic>.fromJson(response.data);
|
||||||
|
if (baseModel.code == 0 && baseModel.data != null) {
|
||||||
|
final dataMap = baseModel.data as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final List<dynamic> listFromServer = dataMap['records'] ?? [];
|
||||||
|
historyList.assignAll(
|
||||||
|
listFromServer
|
||||||
|
.map((item) => ReservationModel.fromJson(item as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
hasData.value = historyList.isNotEmpty;
|
||||||
|
} else {
|
||||||
|
showToast(baseModel.message);
|
||||||
|
_resetData();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('获取历史记录失败: $e');
|
||||||
|
_resetData();
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetData() {
|
||||||
|
historyList.clear();
|
||||||
|
hasData.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pickDate(BuildContext context, bool isStartDate) {
|
||||||
|
// 确定当前操作的日期和临时存储变量
|
||||||
|
final DateTime initialDate = isStartDate ? startDate.value : endDate.value;
|
||||||
|
DateTime tempDate = initialDate;
|
||||||
|
|
||||||
|
// 定义全局的最早可选日期
|
||||||
|
final DateTime globalMinimumDate = DateTime(2025, 12, 1);
|
||||||
|
|
||||||
|
// 动态计算当前选择器的最小/最大日期范围
|
||||||
|
DateTime minimumDate;
|
||||||
|
DateTime? maximumDate; // 声明为可空,因为两个日期都可能没有最大限制
|
||||||
|
|
||||||
|
if (isStartDate) {
|
||||||
|
// 当选择【开始日期】时 它的最小日期就是全局最小日期
|
||||||
|
minimumDate = globalMinimumDate;
|
||||||
|
// 最大日期没有限制
|
||||||
|
maximumDate = null;
|
||||||
|
} else {
|
||||||
|
// 当选择【结束日期】时 它的最小日期不能早于当前的开始日期
|
||||||
|
minimumDate = startDate.value;
|
||||||
|
// 确认结束日期没有最大限制 ---
|
||||||
|
//最大日期没有限制
|
||||||
|
maximumDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.bottomSheet(
|
||||||
|
Container(
|
||||||
|
height: 300,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 顶部的取消和确认按钮
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: const Text('取消', style: TextStyle(color: Colors.grey)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// 4. 确认后,更新对应的日期变量
|
||||||
|
if (isStartDate) {
|
||||||
|
startDate.value = tempDate;
|
||||||
|
// 如果新的开始日期晚于结束日期,自动将结束日期调整为同一天
|
||||||
|
if (tempDate.isAfter(endDate.value)) {
|
||||||
|
endDate.value = tempDate;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
endDate.value = tempDate;
|
||||||
|
}
|
||||||
|
Get.back();
|
||||||
|
|
||||||
|
// 选择日期后自动刷新数据
|
||||||
|
fetchHistoryData();
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'确认',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
// 日期选择器
|
||||||
|
Expanded(
|
||||||
|
child: CupertinoDatePicker(
|
||||||
|
mode: CupertinoDatePickerMode.date,
|
||||||
|
initialDateTime: initialDate,
|
||||||
|
// 应用动态计算好的最小/最大日期
|
||||||
|
minimumDate: minimumDate,
|
||||||
|
maximumDate: maximumDate,
|
||||||
|
onDateTimeChanged: (DateTime newDate) {
|
||||||
|
tempDate = newDate;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.transparent, // 使底部工作表外的区域透明
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
plateNumberController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
249
ln_jq_app/lib/pages/b_page/history/view.dart
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
|
import 'package:ln_jq_app/pages/b_page/history/controller.dart';
|
||||||
|
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
|
||||||
|
|
||||||
|
class HistoryPage extends GetView<HistoryController> {
|
||||||
|
const HistoryPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Get.put(HistoryController());
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('历史记录'), centerTitle: true),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildFilterCard(context),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildSummaryCard(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildListHeader(),
|
||||||
|
Expanded(child: _buildHistoryList()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterCard(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('时间范围', style: TextStyle(fontSize: 14, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildDateField(context, true)),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Text('至'),
|
||||||
|
),
|
||||||
|
Expanded(child: _buildDateField(context, false)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text('车牌号', style: TextStyle(fontSize: 14, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 44,
|
||||||
|
child: TextField(
|
||||||
|
controller: controller.plateNumberController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '请输入车牌号',
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
FocusScope.of(context).unfocus(); // Hide keyboard
|
||||||
|
controller.fetchHistoryData();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.search, size: 20),
|
||||||
|
label: const Text('查询'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 44),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryCard() {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20.0),
|
||||||
|
child: Obx(
|
||||||
|
() => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_buildSummaryItem('实际加氢总量', controller.totalHydrogen.value, Colors.blue),
|
||||||
|
const SizedBox(width: 1, height: 40, child: VerticalDivider()),
|
||||||
|
_buildSummaryItem(
|
||||||
|
'预约完成次数',
|
||||||
|
controller.totalCompletions.value,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHistoryList() {
|
||||||
|
return Obx(() {
|
||||||
|
if (controller.isLoading.value) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (!controller.hasData.value) {
|
||||||
|
return const Center(child: Text('没有找到相关记录'));
|
||||||
|
}
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: controller.historyList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final ReservationModel item = controller.historyList[index];
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text('车牌号: ${item.plateNumber}'),
|
||||||
|
subtitle: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: '加氢站: ${item.stationName}\n',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: '时间: ${item.time}\n',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: '加氢量:',
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: '${item.amount}',
|
||||||
|
style: TextStyle(fontSize: 16, color: AppTheme.themeColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
,
|
||||||
|
trailing:
|
||||||
|
// 状态标签
|
||||||
|
_buildStatusChip(item.status),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusChip(ReservationStatus status) {
|
||||||
|
String text;
|
||||||
|
Color color;
|
||||||
|
switch (status) {
|
||||||
|
case ReservationStatus.pending:
|
||||||
|
text = '待加氢';
|
||||||
|
color = Colors.orange;
|
||||||
|
break;
|
||||||
|
case ReservationStatus.completed:
|
||||||
|
text = '已加氢';
|
||||||
|
color = Colors.greenAccent;
|
||||||
|
break;
|
||||||
|
case ReservationStatus.rejected:
|
||||||
|
text = '拒绝加氢';
|
||||||
|
color = Colors.red;
|
||||||
|
break;
|
||||||
|
case ReservationStatus.unadded:
|
||||||
|
text = '未加氢';
|
||||||
|
color = Colors.red;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text = '未知状态';
|
||||||
|
color = Colors.grey;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.circle, color: color, size: 8),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDateField(BuildContext context, bool isStart) {
|
||||||
|
return Obx(
|
||||||
|
() => InkWell(
|
||||||
|
onTap: () => controller.pickDate(context, isStart),
|
||||||
|
child: Container(
|
||||||
|
height: 44,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.grey.shade400),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(isStart ? controller.formattedStartDate : controller.formattedEndDate),
|
||||||
|
const Icon(Icons.calendar_today, size: 18, color: Colors.grey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryItem(String label, String value, Color color) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(color: color, fontSize: 22, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListHeader() {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 14.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('加氢明细', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,123 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
import 'package:ln_jq_app/pages/login/view.dart';
|
import 'package:ln_jq_app/pages/login/view.dart';
|
||||||
|
|
||||||
|
import '../../../common/styles/theme.dart';
|
||||||
import '../../../storage_service.dart';
|
import '../../../storage_service.dart';
|
||||||
|
|
||||||
class ReservationController extends GetxController with BaseControllerMixin {
|
class ReservationController extends GetxController with BaseControllerMixin {
|
||||||
@override
|
@override
|
||||||
String get builderId => 'b_reservation'; // 确保ID与View中一致
|
String get builderId => 'b_reservation';
|
||||||
|
|
||||||
// --- 运营状态下拉菜单所需的状态 ---
|
|
||||||
// 下拉菜单的选项列表
|
|
||||||
final List<String> operationStatusOptions = ["营运中", "维修中", "暂停营业", "站点关闭"];
|
final List<String> operationStatusOptions = ["营运中", "维修中", "暂停营业", "站点关闭"];
|
||||||
|
|
||||||
// 当前选中的值,默认为'运营中'
|
|
||||||
String selectedOperationStatus = "营运中";
|
String selectedOperationStatus = "营运中";
|
||||||
|
|
||||||
|
// --- 其它状态下的时间选择 ---
|
||||||
|
DateTime? customStartTime;
|
||||||
|
DateTime? customEndTime;
|
||||||
|
|
||||||
|
String get customStartTimeStr => customStartTime != null
|
||||||
|
? DateFormat('yyyy-MM-dd HH:mm').format(customStartTime!)
|
||||||
|
: '点击选择开始时间';
|
||||||
|
|
||||||
|
String get customEndTimeStr => customEndTime != null
|
||||||
|
? DateFormat('yyyy-MM-dd HH:mm').format(customEndTime!)
|
||||||
|
: '点击选择结束时间';
|
||||||
|
|
||||||
|
// --- 站点广播相关 ---
|
||||||
|
final TextEditingController broadcastTitleController = TextEditingController();
|
||||||
|
final TextEditingController broadcastContentController = TextEditingController();
|
||||||
|
final RxInt selectedTabIndex = 0.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
// 1. 初始化默认时间
|
||||||
|
customStartTime = DateTime.now();
|
||||||
|
customEndTime = customStartTime!.add(const Duration(days: 1));
|
||||||
renderData();
|
renderData();
|
||||||
}
|
}
|
||||||
|
|
||||||
String name = "";
|
String name = "";
|
||||||
String address = "";
|
String address = "";
|
||||||
String phone = "";
|
String phone = "";
|
||||||
String costPrice = ""; //氢气价格
|
String costPrice = "";
|
||||||
String customerPrice = ""; //官方价格
|
String customerPrice = "";
|
||||||
String startBusiness = "";
|
String startBusiness = "";
|
||||||
String endBusiness = "";
|
String endBusiness = "";
|
||||||
String timeStr = "";
|
String timeStr = "";
|
||||||
String operatingEnterprise = "";
|
String operatingEnterprise = "";
|
||||||
String hydrogenId = "";
|
String hydrogenId = "";
|
||||||
|
|
||||||
|
String jobTipStr = "";
|
||||||
|
String jobDetailsStr = "";
|
||||||
|
String jobId = "";
|
||||||
|
|
||||||
Future<void> renderData() async {
|
Future<void> renderData() async {
|
||||||
showLoading("加载中");
|
showLoading("加载中");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
//获取加氢站未执行的状态修改任务信息
|
||||||
|
var jobData = await HttpService.to.get('appointment/job/hyd/un-executed');
|
||||||
|
if (jobData != null) {
|
||||||
|
final jobDataResult = BaseModel.fromJson(jobData.data);
|
||||||
|
if (jobDataResult.code == 0) {
|
||||||
|
try{
|
||||||
|
jobId = jobDataResult.data["id"] ?? "";
|
||||||
|
String endTime = jobDataResult.data["endTime"] ?? "";
|
||||||
|
String beginTime = jobDataResult.data["beginTime"] ?? "";
|
||||||
|
String hydStatus = jobDataResult.data["hydStatus"] ?? "";
|
||||||
|
String hydStatusStr = "";
|
||||||
|
if (hydStatus == "0") {
|
||||||
|
hydStatusStr = "营运中";
|
||||||
|
} else if (hydStatus == "1") {
|
||||||
|
hydStatusStr = "维修中";
|
||||||
|
} else if (hydStatus == "2") {
|
||||||
|
hydStatusStr = "站点关闭";
|
||||||
|
} else if (hydStatus == "3") {
|
||||||
|
hydStatusStr = "暂停营业";
|
||||||
|
}
|
||||||
|
|
||||||
|
jobDetailsStr = "当前站点已设置$beginTime-$endTime为$hydStatusStr状态";
|
||||||
|
|
||||||
|
if (endTime.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
// 解析时间字符串
|
||||||
|
DateTime endDateTime = DateTime.parse(endTime);
|
||||||
|
DateTime beginDateTime = DateTime.parse(beginTime);
|
||||||
|
DateTime now = DateTime.now(); // 2. 计算时间差 (endTime - now)
|
||||||
|
Duration diff = endDateTime.difference(now);
|
||||||
|
|
||||||
|
// 计算小时数 (允许小数,例如 0.5)
|
||||||
|
// inMinutes / 60 可以得到更精确的小数小时
|
||||||
|
double hoursLeft = diff.inMinutes / 60.0;
|
||||||
|
|
||||||
|
if (hoursLeft > 0) {
|
||||||
|
// 如果是正数,表示还有多久结束
|
||||||
|
String timeTip = "${hoursLeft.toStringAsFixed(1)}小时后";
|
||||||
|
jobTipStr = "$timeTip$hydStatusStr";
|
||||||
|
} else {
|
||||||
|
jobTipStr = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是处于非营运状态,自动回填开始和结束时间
|
||||||
|
// 假设 customStartTime 是现在,customEndTime 是接口返回的结束时间
|
||||||
|
customStartTime = beginDateTime;
|
||||||
|
customEndTime = endDateTime;
|
||||||
|
} catch (e) {
|
||||||
|
print("时间解析失败: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (e){
|
||||||
|
Logger.d("解析失败: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取站点信息
|
||||||
var responseData = await HttpService.to.get(
|
var responseData = await HttpService.to.get(
|
||||||
'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}',
|
'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}',
|
||||||
);
|
);
|
||||||
@@ -49,32 +130,30 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
|
name = result.data["name"] ?? "";
|
||||||
name = result.data["name"];
|
|
||||||
hydrogenId = result.data["hydrogenId"].toString();
|
hydrogenId = result.data["hydrogenId"].toString();
|
||||||
address = result.data["address"];
|
address = result.data["address"] ?? "";
|
||||||
var rawCostPrice = result.data["costPrice"];
|
|
||||||
if (rawCostPrice != null && rawCostPrice.toString().isNotEmpty) {
|
|
||||||
costPrice = "¥$rawCostPrice";
|
|
||||||
} else {
|
|
||||||
costPrice = "暂无价格";
|
|
||||||
}
|
|
||||||
var customerPriceTemp = result.data["customerPrice"];
|
|
||||||
if (customerPriceTemp != null && customerPriceTemp.toString().isNotEmpty) {
|
|
||||||
customerPrice = "$customerPriceTemp";
|
|
||||||
} else {
|
|
||||||
customerPrice = "暂无价格";
|
|
||||||
}
|
|
||||||
|
|
||||||
phone = result.data["phone"];
|
var rawCostPrice = result.data["costPrice"];
|
||||||
startBusiness = result.data["startBusiness"];
|
costPrice = (rawCostPrice != null && rawCostPrice.toString().isNotEmpty)
|
||||||
endBusiness = result.data["endBusiness"];
|
? "¥$rawCostPrice"
|
||||||
|
: "暂无价格";
|
||||||
|
|
||||||
|
var customerPriceTemp = result.data["customerPrice"];
|
||||||
|
customerPrice =
|
||||||
|
(customerPriceTemp != null && customerPriceTemp.toString().isNotEmpty)
|
||||||
|
? "$customerPriceTemp"
|
||||||
|
: "暂无价格";
|
||||||
|
|
||||||
|
phone = result.data["phone"] ?? "";
|
||||||
|
startBusiness = result.data["startBusiness"] ?? "";
|
||||||
|
endBusiness = result.data["endBusiness"] ?? "";
|
||||||
operatingEnterprise = result.data["operatingEnterprise"].toString();
|
operatingEnterprise = result.data["operatingEnterprise"].toString();
|
||||||
|
|
||||||
String temp = result.data["siteStatusName"].toString();
|
String temp = result.data["siteStatusName"].toString();
|
||||||
selectedOperationStatus = (temp.isEmpty || temp == "null") ? "营运中" : temp;
|
selectedOperationStatus = (temp.isEmpty || temp == "null") ? "营运中" : temp;
|
||||||
|
|
||||||
if (startBusiness.isNotEmpty && endBusiness.isNotEmpty) {
|
if (startBusiness.isNotEmpty && endBusiness.isNotEmpty) {
|
||||||
// 营业时间为24小时的特殊处理
|
|
||||||
if (startBusiness == "00:00:00" && endBusiness == "23:59:59") {
|
if (startBusiness == "00:00:00" && endBusiness == "23:59:59") {
|
||||||
timeStr = "24小时营业";
|
timeStr = "24小时营业";
|
||||||
} else {
|
} else {
|
||||||
@@ -86,19 +165,18 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
operatingEnterprise = operatingEnterprise.isEmpty ? "暂未设置" : operatingEnterprise;
|
operatingEnterprise = operatingEnterprise.isEmpty ? "暂未设置" : operatingEnterprise;
|
||||||
|
|
||||||
updateUi();
|
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 如果解析 JSON 失败
|
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
showToast('数据异常');
|
showToast('数据异常');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
|
} finally {
|
||||||
|
updateUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新运营状态的方法
|
|
||||||
void onOperationStatusChanged(String? newValue) {
|
void onOperationStatusChanged(String? newValue) {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
selectedOperationStatus = newValue;
|
selectedOperationStatus = newValue;
|
||||||
@@ -106,9 +184,99 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveInfo() async {
|
/// 使用底部滚轮形式选择时间(下拉框效果)
|
||||||
showLoading("保存中");
|
void pickDateTime(BuildContext context, bool isStart) {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
DateTime initialDate = isStart ? (customStartTime ?? now) : (customEndTime ?? now);
|
||||||
|
|
||||||
|
DateTime minLimit = isStart
|
||||||
|
? now.subtract(const Duration(minutes: 1))
|
||||||
|
: (customStartTime ?? now).subtract(const Duration(minutes: 1));
|
||||||
|
|
||||||
|
if (initialDate.isBefore(minLimit)) {
|
||||||
|
initialDate = isStart ? now : (customStartTime ?? now);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime tempDate = initialDate;
|
||||||
|
|
||||||
|
Get.bottomSheet(
|
||||||
|
Container(
|
||||||
|
height: 300,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
CupertinoButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: const Text('取消', style: TextStyle(color: Colors.grey)),
|
||||||
|
),
|
||||||
|
CupertinoButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (isStart) {
|
||||||
|
customStartTime = tempDate;
|
||||||
|
if (customEndTime != null &&
|
||||||
|
customEndTime!.isBefore(customStartTime!)) {
|
||||||
|
customEndTime = customStartTime!.add(const Duration(days: 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (tempDate.isBefore(customStartTime ?? DateTime.now())) {
|
||||||
|
showToast('结束时间不能早于开始时间');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customEndTime = tempDate;
|
||||||
|
}
|
||||||
|
updateUi();
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'确定',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.themeColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: CupertinoDatePicker(
|
||||||
|
mode: CupertinoDatePickerMode.dateAndTime,
|
||||||
|
initialDateTime: initialDate,
|
||||||
|
minimumDate: minLimit,
|
||||||
|
use24hFormat: true,
|
||||||
|
onDateTimeChanged: (DateTime newDate) {
|
||||||
|
tempDate = newDate;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveInfo() async {
|
||||||
|
if (selectedOperationStatus != "营运中") {
|
||||||
|
if (customStartTime == null || customEndTime == null) {
|
||||||
|
showToast("请选择开始和结束时间");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading("保存中");
|
||||||
try {
|
try {
|
||||||
var responseData = await HttpService.to.post(
|
var responseData = await HttpService.to.post(
|
||||||
'appointment/station/updateStationStatus',
|
'appointment/station/updateStationStatus',
|
||||||
@@ -123,18 +291,24 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
: selectedOperationStatus == "暂停营业"
|
: selectedOperationStatus == "暂停营业"
|
||||||
? "3"
|
? "3"
|
||||||
: 0,
|
: 0,
|
||||||
|
'beginTime': selectedOperationStatus == "营运中"
|
||||||
|
? null
|
||||||
|
: DateFormat('yyyy-MM-dd HH:mm:ss').format(customStartTime!),
|
||||||
|
'endTime': selectedOperationStatus == "营运中"
|
||||||
|
? null
|
||||||
|
: DateFormat('yyyy-MM-dd HH:mm:ss').format(customEndTime!),
|
||||||
'updateTime': getNowDateTimeString(),
|
'updateTime': getNowDateTimeString(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (responseData == null && responseData!.data == null) {
|
if (responseData == null || responseData.data == null) {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
showToast('服务暂不可用,请稍后');
|
showToast('服务暂不可用,请稍后');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
if (result.code == 0) {
|
if (result.code == 0) {
|
||||||
showSuccessToast("保存成功");
|
showSuccessToast("保存成功,已同步通知对应司机");
|
||||||
}
|
}
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -142,11 +316,99 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void logout() async {
|
/// 显示当前未执行任务的详情弹窗
|
||||||
// TODO: 在这里执行退出登录的逻辑
|
void showJob() {
|
||||||
//清理本地缓存的用户信息 导航到登录页面
|
if (jobDetailsStr.isEmpty) {
|
||||||
await StorageService.to.clearLoginInfo();
|
showToast("当前没有正在生效的任务设置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogX.to.showConfirmDialog(
|
||||||
|
title: '当前设置详情',
|
||||||
|
content: Text(
|
||||||
|
jobDetailsStr,
|
||||||
|
style: const TextStyle(fontSize: 15, height: 1.5),
|
||||||
|
),
|
||||||
|
confirmText: '好的',
|
||||||
|
cancelText: '取消设置',
|
||||||
|
onCancel: () {
|
||||||
|
// 点击“取消设置”调用删除接口
|
||||||
|
_cancelJob();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 内部私有方法:调用取消/删除任务接口
|
||||||
|
void _cancelJob() async {
|
||||||
|
showLoading("正在取消...");
|
||||||
|
try {
|
||||||
|
var response = await HttpService.to.delete(
|
||||||
|
'appointment/job/hyd/$jobId',
|
||||||
|
);
|
||||||
|
|
||||||
|
dismissLoading();
|
||||||
|
if (response != null) {
|
||||||
|
var result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0) {
|
||||||
|
showSuccessToast("已成功取消该设置");
|
||||||
|
// 成功后重新刷新页面数据,重置状态
|
||||||
|
renderData();
|
||||||
|
} else {
|
||||||
|
showErrorToast(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
dismissLoading();
|
||||||
|
showErrorToast("取消失败,请稍后重试");
|
||||||
|
Logger.d("取消任务失败: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送站点广播
|
||||||
|
void sendBroadcast() async {
|
||||||
|
String title = broadcastTitleController.text.trim();
|
||||||
|
String content = broadcastContentController.text.trim();
|
||||||
|
|
||||||
|
if (title.isEmpty) {
|
||||||
|
showToast("请输入通知标题");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (content.isEmpty) {
|
||||||
|
showToast("请输入通知内容");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading("发送中...");
|
||||||
|
try {
|
||||||
|
var responseData = await HttpService.to.post(
|
||||||
|
'appointment/notice/push/station/broadcast',
|
||||||
|
data: {'title': title, 'content': content},
|
||||||
|
);
|
||||||
|
|
||||||
|
dismissLoading();
|
||||||
|
if (responseData != null) {
|
||||||
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
|
if (result.code == 0) {
|
||||||
|
showSuccessToast("广播发送成功");
|
||||||
|
} else {
|
||||||
|
showErrorToast(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
dismissLoading();
|
||||||
|
showToast("发送失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void logout() async {
|
||||||
|
await StorageService.to.clearLoginInfo();
|
||||||
Get.offAll(() => LoginPage());
|
Get.offAll(() => LoginPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
broadcastTitleController.dispose();
|
||||||
|
broadcastContentController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
leading: const Icon(Icons.local_gas_station, color: Colors.blue, size: 40),
|
leading: const Icon(Icons.local_gas_station, color: Colors.blue, size: 40),
|
||||||
title: Text(
|
title: Text(
|
||||||
controller.name,
|
controller.name,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
),
|
),
|
||||||
subtitle: Text(controller.address),
|
subtitle: Text(controller.address),
|
||||||
trailing: Container(
|
trailing: Container(
|
||||||
@@ -57,7 +57,7 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
controller.selectedOperationStatus,
|
controller.selectedOperationStatus,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -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%', '设备状态'),
|
||||||
],
|
],
|
||||||
@@ -100,41 +100,114 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建包含所有信息表单的卡片
|
/// 构建包含所有信息表单的卡片(增加 Tab 切换功能)
|
||||||
Widget _buildInfoFormCard(BuildContext context) {
|
Widget _buildInfoFormCard(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
child: Padding(
|
clipBehavior: Clip.antiAlias, // 确保 Tab 背景圆角生效
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Tab 切换栏
|
||||||
|
Obx(
|
||||||
|
() => Container(
|
||||||
|
color: Colors.grey[50],
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildTabItem(0, Icons.business_outlined, '站点信息'),
|
||||||
|
_buildTabItem(1, Icons.campaign_outlined, '站点广播'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
// 内容区域
|
||||||
|
Obx(
|
||||||
|
() => controller.selectedTabIndex.value == 0
|
||||||
|
? _buildStationInfo(context)
|
||||||
|
: _buildStationBroadcast(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建单个 Tab 项
|
||||||
|
Widget _buildTabItem(int index, IconData icon, String label) {
|
||||||
|
bool isSelected = controller.selectedTabIndex.value == index;
|
||||||
|
return Expanded(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => controller.selectedTabIndex.value = index,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: isSelected ? Colors.blue : Colors.transparent,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20, color: isSelected ? Colors.blue : Colors.grey[600]),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: isSelected ? Colors.blue : Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 站点信息子视图
|
||||||
|
Widget _buildStationInfo(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// --- 基本信息 ---
|
|
||||||
_buildSectionTitle('基本信息'),
|
_buildSectionTitle('基本信息'),
|
||||||
_buildDisplayField(label: '站点名称', value: controller.name),
|
_buildDisplayField(label: '站点名称', value: controller.name),
|
||||||
_buildDisplayField(label: '运营企业', value: controller.operatingEnterprise),
|
_buildDisplayField(label: '运营企业', value: controller.operatingEnterprise),
|
||||||
_buildDisplayField(label: '站点地址', value: controller.address),
|
_buildDisplayField(label: '站点地址', value: controller.address),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// --- 价格信息 ---
|
|
||||||
_buildSectionTitle('价格信息'),
|
_buildSectionTitle('价格信息'),
|
||||||
_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),
|
||||||
|
|
||||||
// --- 运营信息 ---
|
|
||||||
_buildSectionTitle('运营信息'),
|
_buildSectionTitle('运营信息'),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
Text('运营状态', style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
Text('运营状态', style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
||||||
|
//加氢站未执行的状态修改任务
|
||||||
|
if (controller.jobTipStr.isNotEmpty)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: controller.showJob,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
controller.jobTipStr,
|
||||||
|
style: TextStyle(color: Colors.yellow[800], fontSize: 14),
|
||||||
|
),
|
||||||
|
Icon(AntdIcon.question_circle, size: 14, color: Colors.yellow[800]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
//下拉选择框
|
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: controller.selectedOperationStatus,
|
value: controller.selectedOperationStatus,
|
||||||
items: controller.operationStatusOptions.map((String value) {
|
items: controller.operationStatusOptions.map((String value) {
|
||||||
return DropdownMenuItem<String>(
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
||||||
value: value,
|
|
||||||
child: Text(value),
|
|
||||||
);
|
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: controller.onOperationStatusChanged,
|
onChanged: controller.onOperationStatusChanged,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -142,12 +215,26 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 16),
|
||||||
_buildDisplayField(label: '营业时间', value: controller.timeStr),
|
if (controller.selectedOperationStatus == "营运中")
|
||||||
|
_buildDisplayField(label: '营业时间', value: controller.timeStr)
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
_buildClickField(
|
||||||
|
label: '开始时间',
|
||||||
|
value: controller.customStartTimeStr,
|
||||||
|
onTap: () => controller.pickDateTime(context, true),
|
||||||
|
),
|
||||||
|
_buildClickField(
|
||||||
|
label: '结束时间',
|
||||||
|
value: controller.customEndTimeStr,
|
||||||
|
onTap: () => controller.pickDateTime(context, false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
_buildDisplayField(label: '联系电话', value: controller.phone),
|
_buildDisplayField(label: '联系电话', value: controller.phone),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
//保存按钮
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: controller.saveInfo,
|
onPressed: controller.saveInfo,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@@ -158,44 +245,89 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建静态提示信息卡片
|
/// 站点广播子视图
|
||||||
Widget _buildTipsCard() {
|
Widget _buildStationBroadcast(BuildContext context) {
|
||||||
return Card(
|
return Padding(
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoItem(Icons.info_outline, '请确保信息准确无误'),
|
Row(
|
||||||
const SizedBox(height: 10),
|
children: [
|
||||||
_buildInfoItem(Icons.help_outline, '价格信息将实时更新到用户端'),
|
const Icon(Icons.campaign, color: Colors.blue, size: 28),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(width: 10),
|
||||||
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系技术支持: 400-021-1773'),
|
const Text(
|
||||||
|
'站点广播通知',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 13),
|
||||||
|
_buildTextFieldLabel('通知标题'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 45.h,
|
||||||
|
child: TextField(
|
||||||
|
controller: controller.broadcastTitleController,
|
||||||
|
maxLength: 30,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '例如:临时闭站通知',
|
||||||
|
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
counterText: '', // 隐藏原生计数器,我们可以按需自定义
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildTextFieldLabel('通知内容'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: controller.broadcastContentController,
|
||||||
|
maxLength: 150,
|
||||||
|
maxLines: 5,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '请输入通知内容...',
|
||||||
|
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: controller.sendBroadcast,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(double.infinity, 50),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'发送',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建退出登录按钮
|
Widget _buildTextFieldLabel(String label) {
|
||||||
Widget _buildLogoutButton() {
|
return Text(
|
||||||
return ElevatedButton(
|
label,
|
||||||
onPressed: controller.logout,
|
style: const TextStyle(
|
||||||
style: ElevatedButton.styleFrom(
|
fontSize: 14,
|
||||||
backgroundColor: Colors.red,
|
fontWeight: FontWeight.w500,
|
||||||
foregroundColor: Colors.white,
|
color: Colors.black87,
|
||||||
minimumSize: const Size(double.infinity, 48),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
|
||||||
elevation: 2,
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'退出登录',
|
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -227,7 +359,7 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[200], // 使用灰色背景以区分
|
color: Colors.grey[200],
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
border: Border.all(color: Colors.grey[300]!),
|
border: Border.all(color: Colors.grey[300]!),
|
||||||
),
|
),
|
||||||
@@ -241,6 +373,108 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 构建一个“可点击”的选择行
|
||||||
|
Widget _buildClickField({
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
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(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
border: Border.all(color: Colors.blue.withOpacity(0.5)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.black87),
|
||||||
|
),
|
||||||
|
const Icon(Icons.calendar_month, size: 18, color: Colors.blue),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建静态提示信息卡片
|
||||||
|
Widget _buildTipsCard() {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildInfoItem(Icons.info_outline, '请确保信息准确无误'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildInfoItem(Icons.help_outline, '价格信息将实时更新到用户端'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系技术支持: 400-021-1773'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.verified_outlined, color: Colors.blue, size: 20),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: FutureBuilder<String>(
|
||||||
|
future: getVersion(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Text("");
|
||||||
|
}
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
return TextX.labelSmall(
|
||||||
|
"当前版本: ${snapshot.data}",
|
||||||
|
color: Colors.black54,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const Text("");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建退出登录按钮
|
||||||
|
Widget _buildLogoutButton() {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: controller.logout,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(double.infinity, 48),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'退出登录',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 构建带图标的提示信息行
|
/// 构建带图标的提示信息行
|
||||||
Widget _buildInfoItem(IconData icon, String text) {
|
Widget _buildInfoItem(IconData icon, String text) {
|
||||||
return Row(
|
return Row(
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
|
||||||
enum ReservationStatus {
|
enum ReservationStatus {
|
||||||
pending, // 待处理 ( addStatus: 1)
|
pending, // 待处理 ( addStatus: 0)
|
||||||
completed, // 已完成 ( addStatus: 2)
|
completed, // 完成 ( addStatus: 1)
|
||||||
rejected, // 已拒绝 ( 3)
|
rejected, // 拒绝 ( -1)
|
||||||
|
unadded, // 未加 ( 2)
|
||||||
unknown, // 未知状态
|
unknown, // 未知状态
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReservationModel {
|
class ReservationModel {
|
||||||
final String id;
|
final String id;
|
||||||
final String plateNumber;
|
final String plateNumber;
|
||||||
final String amount;
|
String amount;
|
||||||
final String time;
|
final String time;
|
||||||
final String contactPerson;
|
final String contactPerson;
|
||||||
final String contactPhone;
|
final String contactPhone;
|
||||||
@@ -22,6 +26,7 @@ class ReservationModel {
|
|||||||
|
|
||||||
final String contacts;
|
final String contacts;
|
||||||
final String phone;
|
final String phone;
|
||||||
|
final String rejectReason;
|
||||||
final String stationName;
|
final String stationName;
|
||||||
final String startTime;
|
final String startTime;
|
||||||
final String endTime;
|
final String endTime;
|
||||||
@@ -31,6 +36,7 @@ class ReservationModel {
|
|||||||
final String stateName;
|
final String stateName;
|
||||||
final String addStatus;
|
final String addStatus;
|
||||||
final String addStatusName;
|
final String addStatusName;
|
||||||
|
bool hasEdit;
|
||||||
|
|
||||||
ReservationModel({
|
ReservationModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -39,6 +45,7 @@ class ReservationModel {
|
|||||||
required this.time,
|
required this.time,
|
||||||
required this.contactPerson,
|
required this.contactPerson,
|
||||||
required this.contactPhone,
|
required this.contactPhone,
|
||||||
|
required this.hasEdit,
|
||||||
this.status = ReservationStatus.pending,
|
this.status = ReservationStatus.pending,
|
||||||
required this.contacts,
|
required this.contacts,
|
||||||
required this.phone,
|
required this.phone,
|
||||||
@@ -51,13 +58,18 @@ class ReservationModel {
|
|||||||
required this.stateName,
|
required this.stateName,
|
||||||
required this.addStatus,
|
required this.addStatus,
|
||||||
required this.addStatusName,
|
required this.addStatusName,
|
||||||
|
required this.rejectReason,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 工厂构造函数,用于从JSON创建ReservationModel实例
|
/// 工厂构造函数,用于从JSON创建ReservationModel实例
|
||||||
factory ReservationModel.fromJson(Map<String, dynamic> json) {
|
factory ReservationModel.fromJson(Map<String, dynamic> json) {
|
||||||
//1完成 0待处理 2已拒绝
|
// 1完成 2未加 -1拒绝 0是待加氢
|
||||||
ReservationStatus currentStatus;
|
ReservationStatus currentStatus;
|
||||||
int statusFromServer = json['addStatus'] as int? ?? 0;
|
int statusFromServer = json['addStatus'] as int? ?? 0;
|
||||||
|
int state = json['state'] as int? ?? 0;
|
||||||
|
if (state == -1) {
|
||||||
|
currentStatus = ReservationStatus.rejected;
|
||||||
|
} else {
|
||||||
switch (statusFromServer) {
|
switch (statusFromServer) {
|
||||||
case 0:
|
case 0:
|
||||||
currentStatus = ReservationStatus.pending;
|
currentStatus = ReservationStatus.pending;
|
||||||
@@ -66,17 +78,19 @@ class ReservationModel {
|
|||||||
currentStatus = ReservationStatus.completed;
|
currentStatus = ReservationStatus.completed;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
currentStatus = ReservationStatus.rejected;
|
currentStatus = ReservationStatus.unadded;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
currentStatus = ReservationStatus.unknown;
|
currentStatus = ReservationStatus.unknown;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化时间显示
|
// 格式化时间显示
|
||||||
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
|
||||||
: '时间未定';
|
: '时间未定';
|
||||||
|
|
||||||
@@ -102,6 +116,8 @@ class ReservationModel {
|
|||||||
addStatus: statusFromServer.toString(),
|
addStatus: statusFromServer.toString(),
|
||||||
addStatusName: json['addStatusName']?.toString() ?? '',
|
addStatusName: json['addStatusName']?.toString() ?? '',
|
||||||
stateName: json['stateName']?.toString() ?? '',
|
stateName: json['stateName']?.toString() ?? '',
|
||||||
|
rejectReason: json['rejectReason']?.toString() ?? '',
|
||||||
|
hasEdit: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +135,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 +148,7 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
stopAutoRefresh();
|
stopAutoRefresh();
|
||||||
|
searchController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +158,7 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
// 创建一个每5分钟执行一次的周期性定时器
|
// 创建一个每5分钟执行一次的周期性定时器
|
||||||
_refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
|
_refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
|
||||||
fetchReservationData();
|
renderData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,13 +174,16 @@ 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条
|
||||||
|
'keyword': searchText, // 加氢站名称、手机号
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -170,6 +192,7 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
showToast('暂时无法获取预约数据');
|
showToast('暂时无法获取预约数据');
|
||||||
hasReservationData = false;
|
hasReservationData = false;
|
||||||
reservationList = [];
|
reservationList = [];
|
||||||
|
dismissLoading();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +202,7 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
// 【核心修改】处理接口返回的列表数据
|
// 【核心修改】处理接口返回的列表数据
|
||||||
final dataMap = baseModel.data as Map<String, dynamic>;
|
final dataMap = baseModel.data as Map<String, dynamic>;
|
||||||
|
|
||||||
final List<dynamic> listFromServer = dataMap['list'] ?? [];
|
final List<dynamic> listFromServer = dataMap['records'] ?? [];
|
||||||
|
|
||||||
// 使用 .map() 遍历列表,将每个 item 转换为一个 ReservationModel 对象
|
// 使用 .map() 遍历列表,将每个 item 转换为一个 ReservationModel 对象
|
||||||
reservationList = listFromServer.map((item) {
|
reservationList = listFromServer.map((item) {
|
||||||
@@ -208,38 +231,290 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
/// 确认预约
|
/// 确认预约
|
||||||
Future<void> confirmReservation(String id) async {
|
Future<void> confirmReservation(String id) async {
|
||||||
final item = reservationList.firstWhere((item) => item.id == id);
|
final item = reservationList.firstWhere(
|
||||||
DialogX.to.showConfirmDialog(
|
(item) => item.id == id,
|
||||||
title: '确认预约',
|
orElse: () => throw Exception('Reservation not found'),
|
||||||
message: '确定要确认车牌号${item.plateNumber}的预约吗',
|
);
|
||||||
onConfirm: () {
|
final TextEditingController amountController = TextEditingController(
|
||||||
upDataService(id, 1, item);
|
text: item.hydAmount,
|
||||||
|
);
|
||||||
|
Get.dialog(
|
||||||
|
Dialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), // 圆角
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min, // 高度自适应
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'确认加氢状态',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
// content 部分
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'车牌号 ${item.plateNumber}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: AppTheme.themeColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'加氢量',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: TextField(
|
||||||
|
controller: amountController,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.digitsOnly, // 只允许数字输入
|
||||||
|
],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Get.theme.primaryColor,
|
||||||
|
),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
suffixText: 'kg',
|
||||||
|
suffixStyle: TextStyle(fontSize: 16, color: Colors.grey),
|
||||||
|
enabledBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.grey),
|
||||||
|
),
|
||||||
|
focusedBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.grey, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'请选择本次加氢的实际状态\n用于更新预约记录。',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// actions 部分 (按钮)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back(); // 关闭弹窗
|
||||||
|
final num addHydAmount = num.tryParse(amountController.text) ?? 0;
|
||||||
|
upDataService(id, 0, 1, addHydAmount, "", item);
|
||||||
},
|
},
|
||||||
onCancel: () {},
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 48),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('加氢完成', style: TextStyle(fontSize: 16)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back(); // 关闭弹窗
|
||||||
|
upDataService(id, 0, 2, 0, "", item);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
minimumSize: const Size(double.infinity, 48),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('未加氢', style: TextStyle(fontSize: 16)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(), // 只关闭弹窗
|
||||||
|
child: const Text(
|
||||||
|
'暂不处理',
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
barrierDismissible: false, // 点击外部不关闭弹窗
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 拒绝预约
|
|
||||||
Future<void> rejectReservation(String id) async {
|
Future<void> rejectReservation(String id) async {
|
||||||
final item = reservationList.firstWhere((item) => item.id == id);
|
final item = reservationList.firstWhere((item) => item.id == id);
|
||||||
DialogX.to.showConfirmDialog(
|
final TextEditingController reasonController = TextEditingController();
|
||||||
title: '拒绝预约',
|
final RxString selectedReason = ''.obs;
|
||||||
message: '确定要拒绝车牌号${item.plateNumber}的预约吗',
|
|
||||||
onConfirm: () {
|
// 预设的拒绝原因列表
|
||||||
upDataService(id, 2, item);
|
final List<String> presetReasons = ['车辆未到场', '司机联系不上', '设备故障无法加氢'];
|
||||||
|
|
||||||
|
Get.dialog(
|
||||||
|
Dialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'拒绝预约',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'正在处理车牌号 ${item.plateNumber} 的预约',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 16, color: AppTheme.themeColor),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 4. 预设原因选择区域
|
||||||
|
const Text('选择或填写拒绝原因:', style: TextStyle(color: Colors.grey)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Obx(
|
||||||
|
() => Wrap(
|
||||||
|
// 使用 Wrap 自动换行
|
||||||
|
spacing: 8.0, // 水平间距
|
||||||
|
children: presetReasons.map((reason) {
|
||||||
|
final isSelected = selectedReason.value == reason;
|
||||||
|
return ChoiceChip(
|
||||||
|
label: Text(reason),
|
||||||
|
selected: isSelected,
|
||||||
|
onSelected: (selected) {
|
||||||
|
if (selected) {
|
||||||
|
selectedReason.value = reason;
|
||||||
|
reasonController.clear(); // 选择预设原因时,清空自定义输入
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onCancel: () {},
|
selectedColor: Get.theme.primaryColor.withOpacity(0.2),
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: isSelected ? Get.theme.primaryColor : Colors.black,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 5. 自定义原因输入框
|
||||||
|
TextField(
|
||||||
|
controller: reasonController,
|
||||||
|
maxLines: 2,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '输入其它原因',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (text) {
|
||||||
|
if (text.isNotEmpty) {
|
||||||
|
selectedReason.value = ''; // 输入自定义原因时,取消预设选择
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 6. 按钮区域
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// 获取最终的拒绝原因
|
||||||
|
String finalReason = reasonController.text.trim();
|
||||||
|
if (finalReason.isEmpty) {
|
||||||
|
finalReason = selectedReason.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalReason.isEmpty) {
|
||||||
|
showToast('请选择或填写一个拒绝原因');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.back(); // 关闭弹窗
|
||||||
|
upDataService(id, 1, -1, 0, finalReason, item);
|
||||||
|
},
|
||||||
|
child: const Text('确认拒绝', style: TextStyle(color: Colors.red)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: const Text('暂不处理', style: TextStyle(color: Colors.grey)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
barrierDismissible: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void upDataService(String id, int status, ReservationModel item) async {
|
//addStatus 1完成 2未加 -1拒绝
|
||||||
|
void upDataService(
|
||||||
|
String id,
|
||||||
|
int status,
|
||||||
|
int addStatus,
|
||||||
|
num addHydAmount,
|
||||||
|
String rejectReason,
|
||||||
|
ReservationModel item,
|
||||||
|
) async {
|
||||||
showLoading("确认中");
|
showLoading("确认中");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var responseData = await HttpService.to.post(
|
var responseData;
|
||||||
'appointment/orderAddHyd/completeOrder',
|
if (addStatus == -1) {
|
||||||
data: {'id': id, 'addStatus': status},
|
responseData = await HttpService.to.post(
|
||||||
|
'appointment/orderAddHyd/rejectOrder',
|
||||||
|
data: {
|
||||||
|
'id': id,
|
||||||
|
'state': -1, //拒绝使用
|
||||||
|
"rejectReason": rejectReason,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
responseData = await HttpService.to.post(
|
||||||
|
'appointment/orderAddHyd/completeOrder',
|
||||||
|
data: {
|
||||||
|
'id': id,
|
||||||
|
'addStatus': addStatus, //完成使用 完成1,未加2
|
||||||
|
"addHydAmount": addHydAmount,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (responseData == null && responseData!.data == null) {
|
if (responseData == null && responseData!.data == null) {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
@@ -252,12 +527,16 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
|
|
||||||
if (status == 1) {
|
//1完成 2未加 -1拒绝
|
||||||
|
if (addStatus == 1) {
|
||||||
item.status = ReservationStatus.completed;
|
item.status = ReservationStatus.completed;
|
||||||
} else if (status == 2) {
|
item.amount = "${addHydAmount}kg";
|
||||||
|
} else if (addStatus == -1) {
|
||||||
item.status = ReservationStatus.rejected;
|
item.status = ReservationStatus.rejected;
|
||||||
|
} else if (addStatus == 2) {
|
||||||
|
item.status = ReservationStatus.unadded;
|
||||||
}
|
}
|
||||||
updateUi();
|
renderData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
}
|
}
|
||||||
@@ -267,6 +546,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,14 +567,22 @@ 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"].toString();
|
||||||
|
orderUnfinishedAmount = result.data["orderUnfinishedAmount"].toString();
|
||||||
|
|
||||||
leftHydrogen = leftHydrogen.isEmpty ? "统计中" : leftHydrogen.toString();
|
leftHydrogen = leftHydrogen.isEmpty ? "统计中" : leftHydrogen.toString();
|
||||||
|
orderTotalAmount = orderTotalAmount.isEmpty ? "统计中" : orderTotalAmount.toString();
|
||||||
//加载列表数据
|
orderUnfinishedAmount = orderUnfinishedAmount.isEmpty
|
||||||
fetchReservationData();
|
? "统计中"
|
||||||
|
: orderUnfinishedAmount.toString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('数据异常');
|
showToast('数据异常');
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
} finally {
|
||||||
|
|
||||||
|
//加载列表数据
|
||||||
|
fetchReservationData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
|
import 'package:ln_jq_app/pages/b_page/history/view.dart';
|
||||||
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
|
|
||||||
@@ -94,6 +95,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, '未加氢总量'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -103,18 +115,21 @@ class SitePage extends GetView<SiteController> {
|
|||||||
Card(
|
Card(
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: EdgeInsets.only(bottom: 12),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: GestureDetector(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
onTap: () {
|
||||||
|
controller.renderData();
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'今日预约信息',
|
'今日预约信息',
|
||||||
@@ -124,32 +139,46 @@ class SitePage extends GetView<SiteController> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 2),
|
SizedBox(
|
||||||
Text(
|
width: 32,
|
||||||
'Reservation Information',
|
height: 32,
|
||||||
style: TextStyle(fontSize: 12, color: Colors.white70),
|
child: const Icon(
|
||||||
|
Icons.refresh,
|
||||||
|
size: 18,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
),
|
||||||
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.fetchReservationData();
|
Get.to(
|
||||||
|
() => HistoryPage(),
|
||||||
|
arguments: {
|
||||||
|
'stationName': controller.name,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.refresh, size: 16),
|
|
||||||
label: const Text('刷新'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
foregroundColor: Colors.blue,
|
backgroundColor: Colors.blue.shade700,
|
||||||
backgroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(5),
|
||||||
),
|
),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'历史记录',
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_buildSearchView(),
|
||||||
controller.hasReservationData
|
controller.hasReservationData
|
||||||
? _buildReservationListView()
|
? _buildReservationListView()
|
||||||
: _buildEmptyReservationView(),
|
: _buildEmptyReservationView(),
|
||||||
@@ -179,6 +208,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(
|
||||||
@@ -336,7 +425,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () => controller.confirmReservation(item.id),
|
onPressed: () => controller.confirmReservation(item.id),
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
|
||||||
child: const Text('确定'),
|
child: const Text('确认'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@@ -361,15 +450,19 @@ class SitePage extends GetView<SiteController> {
|
|||||||
Color color;
|
Color color;
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ReservationStatus.pending:
|
case ReservationStatus.pending:
|
||||||
text = '待处理';
|
text = '待加氢';
|
||||||
color = Colors.orange;
|
color = Colors.orange;
|
||||||
break;
|
break;
|
||||||
case ReservationStatus.completed:
|
case ReservationStatus.completed:
|
||||||
text = '已完成';
|
text = '已加氢';
|
||||||
color = Colors.greenAccent;
|
color = Colors.greenAccent;
|
||||||
break;
|
break;
|
||||||
case ReservationStatus.rejected:
|
case ReservationStatus.rejected:
|
||||||
text = '已拒绝';
|
text = '拒绝加氢';
|
||||||
|
color = Colors.red;
|
||||||
|
break;
|
||||||
|
case ReservationStatus.unadded:
|
||||||
|
text = '未加氢';
|
||||||
color = Colors.red;
|
color = Colors.red;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
|
|||||||
// 对应的页面
|
// 对应的页面
|
||||||
List<Widget> _buildPages() {
|
List<Widget> _buildPages() {
|
||||||
return [
|
return [
|
||||||
MapPage(),
|
|
||||||
ReservationPage(),
|
ReservationPage(),
|
||||||
|
MapPage(),
|
||||||
CarInfoPage(),
|
CarInfoPage(),
|
||||||
MinePage(),
|
MinePage(),
|
||||||
];
|
];
|
||||||
@@ -47,18 +47,15 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
|
|||||||
jumpTabAndPage(index);
|
jumpTabAndPage(index);
|
||||||
}, // 切换tab事件
|
}, // 切换tab事件
|
||||||
items: [
|
items: [
|
||||||
NavigationItemModel(
|
|
||||||
label: '地图',
|
|
||||||
icon: AntdIcon.location,
|
|
||||||
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(
|
||||||
|
label: '地图',
|
||||||
|
icon: AntdIcon.location,
|
||||||
|
selectedIcon: AntdIcon.location_fill,
|
||||||
),
|
),
|
||||||
NavigationItemModel(
|
NavigationItemModel(
|
||||||
label: '车辆信息',
|
label: '车辆信息',
|
||||||
|
|||||||
@@ -40,11 +40,10 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
|
|||||||
if (controller.fileType == 'pdf') {
|
if (controller.fileType == 'pdf') {
|
||||||
if (controller.localFilePath.isNotEmpty) {
|
if (controller.localFilePath.isNotEmpty) {
|
||||||
return PDFView(
|
return PDFView(
|
||||||
key: ValueKey(controller.localFilePath.value),
|
|
||||||
filePath: controller.localFilePath.value,
|
filePath: controller.localFilePath.value,
|
||||||
enableSwipe: true,
|
enableSwipe: true,
|
||||||
swipeHorizontal: false,
|
swipeHorizontal: true,
|
||||||
autoSpacing: false,
|
autoSpacing: true,
|
||||||
pageFling: true,
|
pageFling: true,
|
||||||
onRender: (pages) {
|
onRender: (pages) {
|
||||||
print("PDF rendered with $pages pages.");
|
print("PDF rendered with $pages pages.");
|
||||||
|
|||||||
@@ -1,21 +1,77 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
import 'attachment_viewer_page.dart';
|
import 'attachment_viewer_page.dart';
|
||||||
|
|
||||||
class CertificateViewerController extends GetxController {
|
class CertificateViewerController extends GetxController with BaseControllerMixin{
|
||||||
late final String title;
|
late final String title;
|
||||||
late final List<String> attachments;
|
late final List<String> attachments;
|
||||||
|
|
||||||
|
// --- 新增: 状态管理 ---
|
||||||
|
/// 用于存储网络PDF的本地路径,key是网络url,value是本地路径
|
||||||
|
final RxMap<String, String> localPdfPaths = <String, String>{}.obs;
|
||||||
|
|
||||||
|
/// 用于跟踪每个附件的加载状态,key是网络url
|
||||||
|
final RxMap<String, bool> isLoading = <String, bool>{}.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get builderId => "certificateviewer";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
// 从 Get.to 的 arguments 中获取标题和附件列表
|
|
||||||
title = Get.arguments['title'] ?? '证件详情';
|
title = Get.arguments['title'] ?? '证件详情';
|
||||||
attachments = List<String>.from(Get.arguments['attachments'] ?? []);
|
attachments = List<String>.from(Get.arguments['attachments'] ?? []);
|
||||||
|
|
||||||
|
// --- 新增: 初始化时开始加载所有PDF ---
|
||||||
|
_loadAllPdfs();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 导航到通用的附件查看器页面
|
/// 遍历所有附件,如果是PDF则进行下载
|
||||||
|
void _loadAllPdfs() {
|
||||||
|
for (var url in attachments) {
|
||||||
|
if (isPdf(url)) {
|
||||||
|
_downloadPdf(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 下载单个PDF文件
|
||||||
|
Future<void> _downloadPdf(String url) async {
|
||||||
|
if (url.isEmpty) return;
|
||||||
|
|
||||||
|
// 开始加载
|
||||||
|
isLoading[url] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final dio = Dio();
|
||||||
|
final Directory tempDir = await getTemporaryDirectory();
|
||||||
|
final String savePath = '${tempDir.path}/${url.split('/').last}';
|
||||||
|
|
||||||
|
// 检查文件是否已存在,避免重复下载
|
||||||
|
if (await File(savePath).exists()) {
|
||||||
|
localPdfPaths[url] = savePath;
|
||||||
|
isLoading[url] = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dio.download(url, savePath);
|
||||||
|
|
||||||
|
// 下载成功后,更新本地路径
|
||||||
|
localPdfPaths[url] = savePath;
|
||||||
|
} catch (e) {
|
||||||
|
print('PDF download error for $url: $e');
|
||||||
|
// 出错时也可以更新状态,以便UI显示错误提示
|
||||||
|
} finally {
|
||||||
|
// 结束加载
|
||||||
|
isLoading[url] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导航到通用的附件查看器页面 (此方法保持不变)
|
||||||
void openAttachment(String url) {
|
void openAttachment(String url) {
|
||||||
if (url.isEmpty) {
|
if (url.isEmpty) {
|
||||||
showErrorToast('附件链接无效');
|
showErrorToast('附件链接无效');
|
||||||
@@ -30,8 +86,10 @@ class CertificateViewerController extends GetxController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查 URL 是否为 PDF,以便在视图中显示不同的图标
|
/// 检查 URL 是否为 PDF (此方法保持不变)
|
||||||
bool isPdf(String url) {
|
bool isPdf(String url) {
|
||||||
return url.toLowerCase().endsWith('.pdf');
|
return url.toLowerCase().endsWith('.pdf');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,114 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart'; // 引入PDFView
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:getx_scaffold/common/common.dart';
|
||||||
|
|
||||||
import 'certificate_viewer_controller.dart';
|
import 'certificate_viewer_controller.dart';
|
||||||
|
|
||||||
class CertificateViewerPage extends GetView<CertificateViewerController> {
|
class CertificateViewerPage extends GetView<CertificateViewerController> {
|
||||||
const CertificateViewerPage({Key? key}) : super(key: key);
|
const CertificateViewerPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(CertificateViewerController());
|
return GetBuilder<CertificateViewerController>(
|
||||||
|
init: CertificateViewerController(),
|
||||||
|
id: 'certificateviewer',
|
||||||
|
builder: (_) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(controller.title)),
|
||||||
title: Text(controller.title),
|
body: Column(
|
||||||
centerTitle: true,
|
children: [
|
||||||
|
SizedBox(height: 16,),
|
||||||
|
Text(
|
||||||
|
"点击可查看大图",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
Expanded(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
itemCount: controller.attachments.length,
|
itemCount: controller.attachments.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final url = controller.attachments[index];
|
final url = controller.attachments[index];
|
||||||
// 从 URL 中提取文件名用于显示
|
return _buildAttachmentItem(url);
|
||||||
final fileName = url.split('/').last;
|
},
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
controller.isPdf(url)
|
|
||||||
? Icons.picture_as_pdf_rounded // PDF 图标
|
|
||||||
: Icons.image_rounded, // 图片图标
|
|
||||||
color: controller.isPdf(url) ? Colors.red.shade700 : Colors.blue.shade700,
|
|
||||||
size: 32,
|
|
||||||
),
|
),
|
||||||
title: Text(
|
|
||||||
fileName,
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16, color: Colors.grey),
|
],
|
||||||
onTap: () => controller.openAttachment(url),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建单个附件的显示项
|
||||||
|
Widget _buildAttachmentItem(String url) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
controller.openAttachment(url);
|
||||||
|
}, // 点击跳转到详情页
|
||||||
|
child: Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
clipBehavior: Clip.antiAlias, // 确保内容不会溢出Card的圆角
|
||||||
|
elevation: 4,
|
||||||
|
// 等比缩放展示
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 4 / 3,
|
||||||
|
child: controller.isPdf(url)
|
||||||
|
? Obx(() {
|
||||||
|
final bool loading = controller.isLoading[url] ?? true;
|
||||||
|
final String? localPath = controller.localPdfPaths[url];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return _buildLoadingIndicator();
|
||||||
|
} else if (localPath != null && localPath.isNotEmpty) {
|
||||||
|
return IgnorePointer(
|
||||||
|
ignoring: true, // 设置为 true 来忽略所有指针事件
|
||||||
|
child: PDFView(
|
||||||
|
fitEachPage: true,
|
||||||
|
filePath: localPath,
|
||||||
|
fitPolicy: FitPolicy.WIDTH,
|
||||||
|
// 适配宽度
|
||||||
|
enableSwipe: false,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
preventLinkNavigation: true, // 顺便禁用PDF内部链接的跳转
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// PDF加载失败
|
||||||
|
return _buildErrorIndicator();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: Image.network(
|
||||||
|
url,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
// 图片加载时显示loading
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return _buildLoadingIndicator();
|
||||||
|
},
|
||||||
|
// 图片加载失败时显示错误
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return _buildErrorIndicator();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助Widget:加载中指示器
|
||||||
|
Widget _buildLoadingIndicator() {
|
||||||
|
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助Widget:错误指示器
|
||||||
|
Widget _buildErrorIndicator() {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: Center(child: Icon(Icons.error_outline, color: Colors.red, size: 48)),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -28,6 +29,36 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
getUserBindCarInfo();
|
getUserBindCarInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onReady() {
|
||||||
|
super.onReady();
|
||||||
|
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
|
||||||
|
if (!StorageService.to.hasShownBindVehicleDialog &&
|
||||||
|
StorageService.to.isLoggedIn &&
|
||||||
|
!StorageService.to.hasVehicleInfo) {
|
||||||
|
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) {
|
||||||
VehicleInfo? bean = StorageService.to.vehicleInfo;
|
VehicleInfo? bean = StorageService.to.vehicleInfo;
|
||||||
@@ -60,7 +91,9 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
// 将解析出的 URL 列表赋值给对应的 RxList
|
// 将解析出的 URL 列表赋值给对应的 RxList
|
||||||
drivingAttachments.assignAll(parseAttachments(data['drivingAttachment']));
|
drivingAttachments.assignAll(parseAttachments(data['drivingAttachment']));
|
||||||
operationAttachments.assignAll(parseAttachments(data['operationAttachment']));
|
operationAttachments.assignAll(parseAttachments(data['operationAttachment']));
|
||||||
hydrogenationAttachments.assignAll(parseAttachments(data['hydrogenationAttachment']));
|
hydrogenationAttachments.assignAll(
|
||||||
|
parseAttachments(data['hydrogenationAttachment']),
|
||||||
|
);
|
||||||
registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
|
registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +103,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
/// 跳转到证件查看页面
|
/// 跳转到证件查看页面
|
||||||
void navigateToCertificateViewer(String title, List<String> attachments) {
|
void navigateToCertificateViewer(String title, List<String> attachments) {
|
||||||
if(!StorageService.to.hasVehicleInfo){
|
if (!StorageService.to.hasVehicleInfo) {
|
||||||
showToast('请先绑定车辆');
|
showToast('请先绑定车辆');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -80,10 +113,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
Get.to(
|
Get.to(
|
||||||
() => const CertificateViewerPage(),
|
() => const CertificateViewerPage(),
|
||||||
arguments: {
|
arguments: {'title': title, 'attachments': attachments},
|
||||||
'title': title,
|
|
||||||
'attachments': attachments,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
151
ln_jq_app/lib/pages/c_page/message/controller.dart
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
|
|
||||||
|
import 'model.dart';
|
||||||
|
|
||||||
|
class MessageController extends GetxController {
|
||||||
|
final RefreshController refreshController = RefreshController(initialRefresh: false);
|
||||||
|
final RxList<MessageModel> messageList = <MessageModel>[].obs;
|
||||||
|
int _pageNum = 1;
|
||||||
|
final int _pageSize = 20;
|
||||||
|
|
||||||
|
// 是否所有已读状态
|
||||||
|
final RxBool allRead = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
_loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadData({bool isRefresh = false}) async {
|
||||||
|
final int targetPage = isRefresh ? 1 : _pageNum + 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Map<String, dynamic> requestData = {
|
||||||
|
'appFlag': 1,
|
||||||
|
'pageNum': targetPage,
|
||||||
|
'pageSize': _pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await HttpService.to.get(
|
||||||
|
'appointment/unread_notice/page',
|
||||||
|
params: requestData,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
throw Exception("Response is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
|
||||||
|
if (result.code != 0) {
|
||||||
|
throw Exception(result.message ?? "Business error");
|
||||||
|
}
|
||||||
|
|
||||||
|
final recordsList = result.data?["records"] as List? ?? [];
|
||||||
|
|
||||||
|
final List<MessageModel> newData = recordsList.map((e) {
|
||||||
|
return MessageModel(
|
||||||
|
id: e['id']?.toString() ?? '',
|
||||||
|
title: e['noticeTitle'] ?? '',
|
||||||
|
content: e['noticeContent'] ?? '',
|
||||||
|
createTime: e['createTime'] ?? '',
|
||||||
|
isRead: int.tryParse(e['isRead']?.toString() ?? '0') ?? 0,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
messageList.assignAll(newData);
|
||||||
|
_pageNum = 1; // 只有成功了才重置页码
|
||||||
|
} else {
|
||||||
|
messageList.addAll(newData);
|
||||||
|
if (newData.isNotEmpty) {
|
||||||
|
_pageNum++; // 只有成功且有数据才+1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateRefreshState(isRefresh, newData.length);
|
||||||
|
|
||||||
|
// 检查是否全部已读
|
||||||
|
_checkAllRead();
|
||||||
|
} catch (e) {
|
||||||
|
Logger.d("加载失败: $e"); // 建议加上日志
|
||||||
|
if (isRefresh) {
|
||||||
|
refreshController.refreshFailed();
|
||||||
|
} else {
|
||||||
|
refreshController.loadFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理 RefreshController 状态
|
||||||
|
void _updateRefreshState(bool isRefresh, int count) {
|
||||||
|
if (isRefresh) {
|
||||||
|
refreshController.refreshCompleted();
|
||||||
|
// 如果刷新后的数据量小于 pageSize,说明没有更多数据了
|
||||||
|
if (count < _pageSize) {
|
||||||
|
refreshController.loadNoData();
|
||||||
|
} else {
|
||||||
|
// 必须重置 footer 状态,否则之前如果是 NoData 状态无法再加载
|
||||||
|
refreshController.resetNoData();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (count == 0) {
|
||||||
|
refreshController.loadNoData();
|
||||||
|
} else {
|
||||||
|
refreshController.loadComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRefresh() => _loadData(isRefresh: true);
|
||||||
|
|
||||||
|
void onLoading() => _loadData(isRefresh: false);
|
||||||
|
|
||||||
|
// 标记全部已读
|
||||||
|
void markAllRead() async {
|
||||||
|
showLoading('正在标记...');
|
||||||
|
try {
|
||||||
|
final response = await HttpService.to.post('appointment/unread_notice/read/all');
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0) {
|
||||||
|
for (var msg in messageList) {
|
||||||
|
msg.isRead = 0;
|
||||||
|
}
|
||||||
|
messageList.refresh();
|
||||||
|
allRead.value = true;
|
||||||
|
|
||||||
|
dismissLoading();
|
||||||
|
showSuccessToast('全部已读');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
dismissLoading();
|
||||||
|
showErrorToast('操作失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记单条已读(弹窗显示时调用)
|
||||||
|
void markRead(MessageModel message) async {
|
||||||
|
if (message.isRead == 0) return;
|
||||||
|
|
||||||
|
final response = await HttpService.to.post(
|
||||||
|
'/appointment/unread_notice/read/${message.id}',
|
||||||
|
);
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0) {
|
||||||
|
message.isRead = 0;
|
||||||
|
messageList.refresh();
|
||||||
|
_checkAllRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkAllRead() {
|
||||||
|
allRead.value = messageList.every((msg) => msg.isRead == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
ln_jq_app/lib/pages/c_page/message/model.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
class MessageModel {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String content;
|
||||||
|
final String createTime;
|
||||||
|
int isRead; // 0: 已读, 1: 未读
|
||||||
|
|
||||||
|
MessageModel({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.content,
|
||||||
|
required this.createTime,
|
||||||
|
required this.isRead,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MessageModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MessageModel(
|
||||||
|
id: json['id']?.toString() ?? '',
|
||||||
|
title: json['title']?.toString() ?? '消息通知',
|
||||||
|
content: json['content']?.toString() ?? '',
|
||||||
|
createTime: json['createTime']?.toString() ?? '',
|
||||||
|
isRead: json['isRead'] as int? ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
150
ln_jq_app/lib/pages/c_page/message/view.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
|
import 'controller.dart';
|
||||||
|
import 'model.dart';
|
||||||
|
|
||||||
|
class MessagePage extends GetView<MessageController> {
|
||||||
|
const MessagePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Get.put(MessageController());
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
|
appBar: AppBar(title: const Text('消息通知'), centerTitle: true),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Obx(() => SmartRefresher(
|
||||||
|
controller: controller.refreshController,
|
||||||
|
enablePullUp: true,
|
||||||
|
onRefresh: controller.onRefresh,
|
||||||
|
onLoading: controller.onLoading,
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
itemCount: controller.messageList.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildMessageItem(context, controller.messageList[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
Obx(() => !controller.allRead.value
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: Colors.white,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controller.markAllRead,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
minimumSize: const Size(double.infinity, 44),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
child: const Text('全部标为已读', style: TextStyle(fontSize: 16, color: Colors.white)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMessageItem(BuildContext context, MessageModel item) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
controller.markRead(item);
|
||||||
|
_showMessageDialog(context, item);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 6, right: 12),
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: item.isRead == 1 ? Colors.grey[300] : const Color(0xFFFAAD14),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.title,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
item.content,
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
item.createTime,
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMessageDialog(BuildContext context, MessageModel item) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (context) {
|
||||||
|
return Dialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.title,
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
item.content,
|
||||||
|
style: const TextStyle(fontSize: 15, height: 1.5, color: Colors.black87),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: const BorderSide(color: Colors.blue),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||||
|
),
|
||||||
|
child: const Text('确认', style: TextStyle(color: Colors.blue)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
renderData();
|
renderData();
|
||||||
renderViolation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -31,6 +30,7 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String rate = "";
|
String rate = "";
|
||||||
|
String rating = "";
|
||||||
String accident = "";
|
String accident = "";
|
||||||
String historyBreakRules = "";
|
String historyBreakRules = "";
|
||||||
String vin = "";
|
String vin = "";
|
||||||
@@ -38,6 +38,7 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
String violationTotal = "0";
|
String violationTotal = "0";
|
||||||
String violationScore = "0";
|
String violationScore = "0";
|
||||||
String violationDispose = "0";
|
String violationDispose = "0";
|
||||||
|
bool isNotice = false;
|
||||||
|
|
||||||
void renderData() async {
|
void renderData() async {
|
||||||
if (StorageService.to.hasVehicleInfo) {
|
if (StorageService.to.hasVehicleInfo) {
|
||||||
@@ -49,14 +50,17 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
plateNumber = bean.plateNumber;
|
plateNumber = bean.plateNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoading("加载中");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
showLoading("加载中");
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
_fetchCompletionRate(), // 请求1:完成率
|
_fetchCompletionRate(), // 请求1:完成率
|
||||||
_fetchAccidentCount(), // 请求2:事故数
|
_fetchAccidentCount(), // 请求2:事故数
|
||||||
_fetchBreakRulesCount(), // 请求3:违章数
|
_fetchBreakRulesCount(), // 请求3:违章数
|
||||||
|
_rating(), // 司机评分
|
||||||
|
_msgNotice(), // 红点消息
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await renderViolation();
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
showErrorToast("加载数据失败,请稍后重试 $e");
|
showErrorToast("加载数据失败,请稍后重试 $e");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -65,6 +69,37 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _msgNotice() async {
|
||||||
|
final Map<String, dynamic> requestData = {
|
||||||
|
'appFlag': 1,
|
||||||
|
'isRead': 1,
|
||||||
|
'pageNum': 1,
|
||||||
|
'pageSize': 5,
|
||||||
|
};
|
||||||
|
final response = await HttpService.to.get(
|
||||||
|
'appointment/unread_notice/page',
|
||||||
|
params: requestData,
|
||||||
|
);
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0 && result.data != null) {
|
||||||
|
String total = result.data["total"].toString();
|
||||||
|
isNotice = int.parse(total) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _rating() async {
|
||||||
|
final response = await HttpService.to.get('appointment/rating/driver');
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0 && result.data != null) {
|
||||||
|
rating = result.data.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _fetchCompletionRate() async {
|
Future<void> _fetchCompletionRate() async {
|
||||||
final response = await HttpService.to.get(
|
final response = await HttpService.to.get(
|
||||||
'appointment/orderAddHyd/driverAppointmentCompletionRate?phone=${StorageService.to.phone}',
|
'appointment/orderAddHyd/driverAppointmentCompletionRate?phone=${StorageService.to.phone}',
|
||||||
@@ -104,15 +139,11 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void renderViolation() async {
|
Future<void> renderViolation() async {
|
||||||
// 违章信息查询
|
// 违章信息查询
|
||||||
final originalHeaders = Map<String, dynamic>.from(HttpService.to.dio.options.headers);
|
|
||||||
try {
|
try {
|
||||||
HttpService.to.setBaseUrl(AppTheme.jiaqing_service_url);
|
|
||||||
HttpService.to.dio.options.headers['appId'] = '97ad10eeb6b346f79e0d6ffd81e4d3c3';
|
|
||||||
|
|
||||||
var responseData = await HttpService.to.get(
|
var responseData = await HttpService.to.get(
|
||||||
"vehicleService/violation/queryViolationInfo_V2?plateNum=${plateNumber}",
|
"appointment/truck/queryViolationInfo_V2?plateNum=${plateNumber}",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (responseData == null || responseData.data == null) {
|
if (responseData == null || responseData.data == null) {
|
||||||
@@ -139,8 +170,6 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
} finally {
|
} finally {
|
||||||
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
|
||||||
HttpService.to.dio.options.headers = originalHeaders;
|
|
||||||
updateUi();
|
updateUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:getx_scaffold/common/index.dart';
|
||||||
|
import 'package:getx_scaffold/common/widgets/index.dart';
|
||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
|
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
|
|
||||||
@@ -78,27 +81,28 @@ class MinePage extends GetView<MineController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
IconButton(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
onPressed: () {
|
||||||
decoration: BoxDecoration(
|
// 跳转消息中心
|
||||||
color: Colors.blue[50],
|
Get.to(() => const MessagePage());
|
||||||
borderRadius: BorderRadius.circular(12),
|
},
|
||||||
border: Border.all(color: Colors.blue, width: 0.5),
|
// 这里的 style 是为了模拟你图片里的灰色圆形背景
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[100],
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
),
|
),
|
||||||
child: const Row(
|
icon: Badge(
|
||||||
children: [
|
// label: Text('3'), // 如果你想显示数字,就加 label
|
||||||
Icon(Icons.shield_outlined, color: Colors.blue, size: 14),
|
smallSize: 8,
|
||||||
SizedBox(width: 4),
|
// 红点的大小
|
||||||
Text(
|
backgroundColor: controller.isNotice ? Colors.red : Colors.white,
|
||||||
'已认证',
|
// 红点颜色
|
||||||
style: TextStyle(
|
child: Icon(
|
||||||
color: Colors.blue,
|
Icons.notifications_outlined,
|
||||||
fontSize: 10,
|
color: Colors.black87,
|
||||||
fontWeight: FontWeight.bold,
|
size: 25,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -155,14 +159,14 @@ class MinePage extends GetView<MineController> {
|
|||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
value: 10, // 假设得分80%
|
value: (double.tryParse(controller.rating) ?? 0) / 10,
|
||||||
strokeWidth: 8,
|
strokeWidth: 8,
|
||||||
backgroundColor: Colors.grey[200],
|
backgroundColor: Colors.grey[200],
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'10',
|
controller.rating,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -311,6 +315,35 @@ class MinePage extends GetView<MineController> {
|
|||||||
_buildInfoItem(Icons.rule, '遵守交通规则,避免违章扣分'),
|
_buildInfoItem(Icons.rule, '遵守交通规则,避免违章扣分'),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
|
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.verified_outlined, color: Colors.blue, size: 20),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: FutureBuilder<String>(
|
||||||
|
future: getVersion(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
// 判断是否还在加载
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Text("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果加载完成且有数据
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
return TextX.labelSmall(
|
||||||
|
"当前版本: ${snapshot.data}",
|
||||||
|
color: Colors.black54,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
return const Text("");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -9,35 +11,81 @@ 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';
|
||||||
|
import 'reservation_list_bottomsheet.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(
|
// 计算当前时间属于哪个1小时区间
|
||||||
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: 60)),
|
||||||
|
);
|
||||||
|
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) {
|
||||||
|
return TimeOfDay(hour: now.hour, minute: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 静态辅助方法,将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;
|
||||||
|
|
||||||
|
// 获取今天的日期 (不含时间)
|
||||||
|
final DateTime today = DateTime(
|
||||||
|
DateTime.now().year,
|
||||||
|
DateTime.now().month,
|
||||||
|
DateTime.now().day,
|
||||||
|
);
|
||||||
|
|
||||||
|
//计算明天的日期
|
||||||
|
final DateTime tomorrow = today.add(const Duration(days: 1));
|
||||||
|
|
||||||
Get.bottomSheet(
|
Get.bottomSheet(
|
||||||
Container(
|
Container(
|
||||||
height: 300,
|
height: 300,
|
||||||
@@ -65,8 +113,21 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
),
|
),
|
||||||
CupertinoButton(
|
CupertinoButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
final bool isChangingToToday =
|
||||||
|
tempDate.isAtSameMomentAs(today) &&
|
||||||
|
!selectedDate.value.isAtSameMomentAs(today);
|
||||||
|
final bool isDateChanged = !tempDate.isAtSameMomentAs(
|
||||||
|
selectedDate.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 更新选中的日期
|
||||||
selectedDate.value = tempDate;
|
selectedDate.value = tempDate;
|
||||||
Get.back();
|
Get.back(); // 先关闭弹窗
|
||||||
|
|
||||||
|
// 如果日期发生了变化,则重置时间
|
||||||
|
if (isDateChanged) {
|
||||||
|
resetTimeForSelectedDate();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'确认',
|
'确认',
|
||||||
@@ -83,28 +144,12 @@ 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(
|
minimumDate: today,
|
||||||
DateTime(
|
// 最小可选日期为今天
|
||||||
DateTime.now().year,
|
maximumDate: tomorrow,
|
||||||
DateTime.now().month,
|
// 最大可选日期为明天
|
||||||
DateTime.now().day,
|
// ---------------------
|
||||||
),
|
|
||||||
)
|
|
||||||
? DateTime(
|
|
||||||
DateTime.now().year,
|
|
||||||
DateTime.now().month,
|
|
||||||
DateTime.now().day,
|
|
||||||
)
|
|
||||||
: selectedDate.value,
|
|
||||||
|
|
||||||
// 设置最小可选日期为“今天凌晨0点”
|
|
||||||
minimumDate: DateTime(
|
|
||||||
DateTime.now().year,
|
|
||||||
DateTime.now().month,
|
|
||||||
DateTime.now().day,
|
|
||||||
),
|
|
||||||
maximumDate: DateTime.now().add(const Duration(days: 365)),
|
|
||||||
onDateTimeChanged: (DateTime newDate) {
|
onDateTimeChanged: (DateTime newDate) {
|
||||||
tempDate = newDate;
|
tempDate = newDate;
|
||||||
},
|
},
|
||||||
@@ -117,69 +162,94 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void pickTime(BuildContext context, bool isStartTime) {
|
void resetTimeForSelectedDate() {
|
||||||
// 确定当前操作的时间和初始值
|
final now = DateTime.now();
|
||||||
TimeOfDay initialTime = isStartTime ? startTime.value : endTime.value;
|
|
||||||
DateTime now = DateTime.now();
|
|
||||||
|
|
||||||
// 计算最小可选时间
|
|
||||||
DateTime? minimumDateTime; // 默认为 null,即可选任意时间
|
|
||||||
|
|
||||||
// 获取当前选择的日期(年月日)
|
|
||||||
final selectedDay = DateTime(
|
|
||||||
selectedDate.value.year,
|
|
||||||
selectedDate.value.month,
|
|
||||||
selectedDate.value.day,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 获取今天的日期(年月日)
|
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
if (isStartTime) {
|
// 判断新选择的日期是不是今天
|
||||||
// 如果是选择开始时间并且日期是 今天
|
if (selectedDate.value.isAtSameMomentAs(today)) {
|
||||||
if (selectedDay.isAtSameMomentAs(today)) {
|
// 如果是今天,就将时间重置为当前时间所在的半小时区间
|
||||||
minimumDateTime = now; // 最小可选时间就是现在
|
startTime.value = _calculateInitialStartTime(now);
|
||||||
}
|
endTime.value = TimeOfDay.fromDateTime(
|
||||||
|
_getDateTimeFromTimeOfDay(startTime.value).add(const Duration(minutes: 30)),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// 如果是选择结束时间
|
// 如果是明天(或其他未来日期),则可以将时间重置为一天的最早可用时间,例如 00:00
|
||||||
// 将开始时间转换为 DateTime 对象
|
startTime.value = const TimeOfDay(hour: 0, minute: 0);
|
||||||
final startDateTime = DateTime(
|
endTime.value = const TimeOfDay(hour: 0, minute: 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///60 分钟为间隔 时间选择器
|
||||||
|
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 < 24; i++) {
|
||||||
|
// 每次增加 60 分钟
|
||||||
|
final startMinutes = i * 60;
|
||||||
|
final endMinutes = startMinutes + 60;
|
||||||
|
|
||||||
|
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
|
||||||
|
// 注意:endMinutes % 60 始终为 0,因为间隔是整小时
|
||||||
|
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
|
||||||
|
|
||||||
|
// 如果不是今天,所有时间段都有效
|
||||||
|
if (!isToday) {
|
||||||
|
availableSlots.add(TimeSlot(startTime, endTime));
|
||||||
|
} else {
|
||||||
|
// 如果是今天,需要判断该时间段是否可选
|
||||||
|
// 创建时间段的结束时间对象
|
||||||
|
final slotEndDateTime = DateTime(
|
||||||
selectedDate.value.year,
|
selectedDate.value.year,
|
||||||
selectedDate.value.month,
|
selectedDate.value.month,
|
||||||
selectedDate.value.day,
|
selectedDate.value.day,
|
||||||
startTime.value.hour,
|
endTime.hour,
|
||||||
startTime.value.minute,
|
endTime.minute,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 结束时间的最小值必须晚于开始时间
|
// 注意:如果是跨天的 00:00 (例如 23:00 - 00:00),需要将日期加一天,否则 isAfter 判断会出错
|
||||||
minimumDateTime = startDateTime;
|
// 但由于我们用的是 endTime.hour % 24,当变成 0 时,日期还是 selectedDate
|
||||||
|
// 这里做一个特殊处理:如果 endTime 是 00:00,意味着它实际上是明天的开始
|
||||||
|
DateTime realEndDateTime = slotEndDateTime;
|
||||||
|
if (endTime.hour == 0 && endTime.minute == 0) {
|
||||||
|
realEndDateTime = slotEndDateTime.add(const Duration(days: 1));
|
||||||
|
}
|
||||||
|
|
||||||
// 如果日期是今天,并且开始时间早于现在,那么结束时间的最小值也应该是现在
|
// 只要时间段的结束时间晚于当前时间,这个时间段就是可预约的
|
||||||
if (selectedDay.isAtSameMomentAs(today) && startDateTime.isBefore(now)) {
|
if (realEndDateTime.isAfter(now)) {
|
||||||
minimumDateTime = now;
|
availableSlots.add(TimeSlot(startTime, endTime));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始显示时间
|
if (availableSlots.isEmpty) {
|
||||||
DateTime initialDateTime = DateTime(
|
showToast('今天已没有可预约的时间段');
|
||||||
selectedDate.value.year,
|
return;
|
||||||
selectedDate.value.month,
|
}
|
||||||
selectedDate.value.day,
|
|
||||||
initialTime.hour,
|
// 查找当前选中的时间对应的新列表中的索引
|
||||||
initialTime.minute,
|
int initialItem = availableSlots.indexWhere(
|
||||||
|
(slot) => slot.start.hour == startTime.value.hour,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 确保初始时间不早于最小时间
|
if (initialItem == -1) {
|
||||||
if (minimumDateTime != null && initialDateTime.isBefore(minimumDateTime)) {
|
initialItem = 0;
|
||||||
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 +273,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 +290,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 +308,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用于存储上一次成功预约的信息
|
||||||
|
ReservationModel? lastSuccessfulReservation;
|
||||||
|
|
||||||
/// 提交预约
|
/// 提交预约
|
||||||
void submitReservation() async {
|
void submitReservation() async {
|
||||||
if (plateNumber.isEmpty) {
|
if (plateNumber.isEmpty) {
|
||||||
@@ -300,33 +322,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',
|
||||||
@@ -342,50 +393,93 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
'hydAmount': ampuntStr,
|
'hydAmount': ampuntStr,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
var result = BaseModel.fromJson(responseData?.data);
|
||||||
|
|
||||||
if (responseData == null) {
|
|
||||||
|
if (responseData == null || result.code != 0) {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
showToast('服务暂不可用,请稍后');
|
showToast(result.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
|
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
|
||||||
if (result.code == 0) {
|
if (result.code == 0) {
|
||||||
showSuccessToast("预约成功");
|
showSuccessToast("预约成功");
|
||||||
|
|
||||||
|
lastSuccessfulReservation = ReservationModel(
|
||||||
|
id: selectedStationId.value!,
|
||||||
|
hydAmount: ampuntStr,
|
||||||
|
startTime: startTimeStr,
|
||||||
|
endTime: endTimeStr,
|
||||||
|
stationName: selectedStation.name,
|
||||||
|
plateNumber: '',
|
||||||
|
amount: '',
|
||||||
|
time: '',
|
||||||
|
contactPerson: '',
|
||||||
|
contactPhone: '',
|
||||||
|
contacts: '',
|
||||||
|
phone: '',
|
||||||
|
date: '',
|
||||||
|
state: '',
|
||||||
|
stateName: '',
|
||||||
|
addStatus: '',
|
||||||
|
addStatusName: '',
|
||||||
|
rejectReason: '',
|
||||||
|
hasEdit: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
//打开预约列表
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
getReservationList(showPopup: true, addStatus: '');
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
showErrorToast(result.message);
|
showToast(result.error);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
showToast('服务暂不可用,请稍后');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 状态变量:是否有预约数据
|
/// 状态变量:是否有预约数据
|
||||||
bool hasReservationData = false;
|
final RxBool hasReservationData = false.obs;
|
||||||
|
|
||||||
// 新增预约数据列表
|
// 新增预约数据列表
|
||||||
List<ReservationModel> reservationList = [];
|
final RxList<ReservationModel> reservationList = <ReservationModel>[].obs;
|
||||||
|
final RxBool shouldShowReservationList = false.obs;
|
||||||
|
|
||||||
|
// --- 用于防抖的 Timer ---
|
||||||
|
Timer? _debounce;
|
||||||
|
|
||||||
//查看预约列表
|
//查看预约列表
|
||||||
void getReservationList() async {
|
void getReservationList({bool showPopup = false, String? addStatus}) async {
|
||||||
|
// 增加 addStatus 参数
|
||||||
|
if (_debounce?.isActive ?? false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_debounce = Timer(const Duration(seconds: 1), () {});
|
||||||
|
|
||||||
showLoading("加载中");
|
showLoading("加载中");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
final Map<String, dynamic> requestData = {
|
||||||
|
'phone': StorageService.to.phone,
|
||||||
|
'pageNum': 1,
|
||||||
|
'pageSize': 50,
|
||||||
|
};
|
||||||
|
// 将 addStatus 参数添加到请求中
|
||||||
|
if (addStatus != null && addStatus.isNotEmpty) {
|
||||||
|
requestData['addStatus'] = addStatus;
|
||||||
|
}
|
||||||
|
|
||||||
var response = await HttpService.to.post(
|
var response = await HttpService.to.post(
|
||||||
"appointment/orderAddHyd/driverOrderPage",
|
"appointment/orderAddHyd/driverOrderPage",
|
||||||
data: {
|
data: requestData,
|
||||||
'phone': StorageService.to.phone, // 使用从 renderData 中获取到的 name
|
|
||||||
'pageNum': 1,
|
|
||||||
'pageSize': 50, // 暂时不考虑分页,一次获取30条
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response == null || response.data == null) {
|
if (response == null || response.data == null) {
|
||||||
showToast('暂时无法获取预约数据');
|
showToast('暂时无法获取预约数据');
|
||||||
hasReservationData = false;
|
hasReservationData.value = false;
|
||||||
reservationList = [];
|
reservationList.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,194 +487,65 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
if (baseModel.code == 0 && baseModel.data != null) {
|
if (baseModel.code == 0 && baseModel.data != null) {
|
||||||
final dataMap = baseModel.data as Map<String, dynamic>;
|
final dataMap = baseModel.data as Map<String, dynamic>;
|
||||||
final List<dynamic> listFromServer = dataMap['list'] ?? [];
|
final List<dynamic> listFromServer = dataMap['records'] ?? [];
|
||||||
reservationList = listFromServer.map((item) {
|
|
||||||
|
// 使用 .value 来更新响应式列表
|
||||||
|
reservationList.value = listFromServer.map((item) {
|
||||||
return ReservationModel.fromJson(item as Map<String, dynamic>);
|
return ReservationModel.fromJson(item as Map<String, dynamic>);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
// 根据列表是否为空来更新 hasReservationData 状态
|
// 更新 hasEdit 状态
|
||||||
hasReservationData = reservationList.isNotEmpty;
|
for (var reservation in reservationList) {
|
||||||
|
try {
|
||||||
|
// 获取当前时间和预约的结束时间
|
||||||
|
final now = DateTime.now();
|
||||||
|
final endDateTime = DateTime.parse(reservation.endTime);
|
||||||
|
|
||||||
|
// 如果当前时间在结束时间之后,则不能编辑
|
||||||
|
if (now.isAfter(endDateTime) ||
|
||||||
|
plateNumber.isEmpty ||
|
||||||
|
reservation.addStatus != "0") {
|
||||||
|
reservation.hasEdit = false;
|
||||||
} else {
|
} else {
|
||||||
showToast(baseModel.message);
|
reservation.hasEdit = true;
|
||||||
hasReservationData = false;
|
|
||||||
reservationList = []; // 清空列表
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
reservation.hasEdit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasReservationData.value = reservationList.isNotEmpty;
|
||||||
|
|
||||||
|
if (showPopup) {
|
||||||
|
shouldShowReservationList.value = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast(baseModel.message);
|
||||||
|
hasReservationData.value = false;
|
||||||
|
reservationList.clear();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.d("${e.toString()}");
|
||||||
showToast('获取预约数据失败');
|
showToast('获取预约数据失败');
|
||||||
hasReservationData = false;
|
hasReservationData.value = false;
|
||||||
reservationList = []; // 清空列表
|
reservationList.clear();
|
||||||
} finally {
|
} finally {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
Get.bottomSheet(
|
|
||||||
Container(
|
|
||||||
height: Get.height * 0.55,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
//标题
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'我的预约',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
elevation: 0,
|
|
||||||
backgroundColor: Colors.grey[200],
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: const Text('关闭', style: TextStyle(color: Colors.black54)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
Expanded(
|
|
||||||
child: !hasReservationData
|
|
||||||
? Container(
|
|
||||||
margin: EdgeInsets.only(top: 40),
|
|
||||||
child: TextX.bodyLarge('暂无预约', weight: FontWeight.w500),
|
|
||||||
)
|
|
||||||
: ListView.builder(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
itemCount: reservationList.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final ReservationModel reservation = reservationList[index];
|
|
||||||
return Card(
|
|
||||||
color: Colors.white,
|
|
||||||
margin: const EdgeInsets.only(bottom: 12.0),
|
|
||||||
elevation: 1,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(10),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 5,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(
|
|
||||||
0xFFE6F7FF,
|
|
||||||
), // Light blue background
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
border: Border.all(
|
|
||||||
color: const Color(0xFF91D5FF),
|
|
||||||
), // Blue border
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
reservation.stateName,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Color(0xFF1890FF),
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(
|
|
||||||
0xFFFFF7E6,
|
|
||||||
), // Light orange background
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
reservation.addStatusName,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Color(0xFFFA8C16),
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildDetailRow('车牌号:', reservation.plateNumber),
|
|
||||||
_buildDetailRow('预约日期:', reservation.date),
|
|
||||||
_buildDetailRow('预约氢量:', reservation.hydAmount),
|
|
||||||
_buildDetailRow('加氢站:', reservation.stationName),
|
|
||||||
_buildDetailRow('开始时间:', reservation.startTime),
|
|
||||||
_buildDetailRow('结束时间:', reservation.endTime),
|
|
||||||
_buildDetailRow('联系人:', reservation.contacts),
|
|
||||||
_buildDetailRow('联系电话:', reservation.phone),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor:
|
|
||||||
Colors.transparent, // Make background transparent to see the rounded corners
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetailRow(String label, String value) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 85,
|
|
||||||
child: Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
value,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String 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 = "";
|
||||||
|
|
||||||
|
//用来管理查看预约的弹窗
|
||||||
|
Worker? _sheetWorker;
|
||||||
|
bool init = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get listenLifecycleEvent => true;
|
bool get listenLifecycleEvent => true;
|
||||||
@@ -590,6 +555,32 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
super.onInit();
|
super.onInit();
|
||||||
getUserBindCarInfo();
|
getUserBindCarInfo();
|
||||||
getSiteList();
|
getSiteList();
|
||||||
|
|
||||||
|
if (!init) {
|
||||||
|
_setupListener();
|
||||||
|
init = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_sheetWorker?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupListener() {
|
||||||
|
_sheetWorker = ever(shouldShowReservationList, (bool shouldShow) {
|
||||||
|
if (shouldShow) {
|
||||||
|
Get.bottomSheet(
|
||||||
|
const ReservationListBottomSheet(),
|
||||||
|
isScrollControlled: true, // 允许弹窗使用更多屏幕高度
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 重要:显示后立即将信号重置为 false,防止不必要的重复弹出
|
||||||
|
shouldShowReservationList.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void getUserBindCarInfo() {
|
void getUserBindCarInfo() {
|
||||||
@@ -601,11 +592,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 = num.tryParse(bean.maxHydrogen) ?? 0;
|
||||||
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 +651,14 @@ 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);
|
||||||
|
|
||||||
|
int flooredDifference = (maxHydrogen - leftHydrogenNum).floor();
|
||||||
|
if (flooredDifference > 0) {
|
||||||
|
amountController.text = flooredDifference.toString();
|
||||||
|
}
|
||||||
|
|
||||||
updateUi();
|
updateUi();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
} finally {
|
} finally {
|
||||||
@@ -659,21 +667,37 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void getSiteList() async {
|
void getSiteList() async {
|
||||||
showLoading("加载中");
|
if (StorageService.to.phone == "13344444444") {
|
||||||
final originalHeaders = Map<String, dynamic>.from(HttpService.to.dio.options.headers);
|
//该账号给stationOptions手动添加一个数据
|
||||||
try {
|
final testStation = StationModel(
|
||||||
HttpService.to.setBaseUrl(AppTheme.jiaqing_service_url);
|
hydrogenId: '1142167389150920704',
|
||||||
HttpService.to.dio.options.headers['appId'] = '97ad10eeb6b346f79e0d6ffd81e4d3c3';
|
name: '演示加氢站',
|
||||||
|
address: '上海市嘉定区于田南路111号于田大厦',
|
||||||
|
price: '35.00',
|
||||||
|
// 价格
|
||||||
|
siteStatusName: '营运中',
|
||||||
|
// 状态
|
||||||
|
isSelect: 1, // 默认可选
|
||||||
|
);
|
||||||
|
// 使用 assignAll 可以确保列表只包含这个测试数据
|
||||||
|
stationOptions.assignAll([testStation]);
|
||||||
|
|
||||||
var responseData = await HttpService.to.get("hydrogen/queryHydrogenSiteInfo");
|
if (stationOptions.isNotEmpty) {
|
||||||
|
selectedStationId.value = stationOptions.first.hydrogenId;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoading("加氢站数据加载中");
|
||||||
|
|
||||||
|
var responseData = await HttpService.to.get("appointment/station/queryHydrogenSiteInfo");
|
||||||
|
|
||||||
if (responseData == null || responseData.data == null) {
|
if (responseData == null || responseData.data == null) {
|
||||||
showToast('暂时无法获取站点信息');
|
showToast('暂时无法获取站点信息');
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
var stationDataList = result.data['data'] as List;
|
var stationDataList = result.data['data'] as List;
|
||||||
@@ -699,14 +723,40 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
} else {
|
} else {
|
||||||
showToast('站点列表已刷新');
|
showToast('站点列表已刷新');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
showToast('数据异常');
|
// 找到第一个可选的站点作为默认值
|
||||||
|
if (stationOptions.isNotEmpty) {
|
||||||
|
final firstSelectable = stationOptions.firstWhere(
|
||||||
|
(station) => station.isSelect == 1,
|
||||||
|
orElse: () => stationOptions.first, // 降级:如果没有可选的,就用第一个
|
||||||
|
);
|
||||||
|
selectedStationId.value = firstSelectable.hydrogenId;
|
||||||
|
} else {
|
||||||
|
// 如果列表为空,确保 selectedStationId 也为空
|
||||||
|
selectedStationId.value = null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
dismissLoading();
|
||||||
|
showToast('数据异常');
|
||||||
} finally {
|
} finally {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
|
||||||
HttpService.to.dio.options.headers = originalHeaders;
|
if (!StorageService.to.hasShownBindVehicleDialog &&
|
||||||
|
StorageService.to.isLoggedIn &&
|
||||||
|
!StorageService.to.hasVehicleInfo) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
DialogX.to.showConfirmDialog(
|
||||||
|
title: '当前尚未绑定车辆',
|
||||||
|
confirmText: "去绑定",
|
||||||
|
cancelText: "稍后",
|
||||||
|
onConfirm: () {
|
||||||
|
doQrCode();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// 标记为已显示,本次会话不再提示
|
||||||
|
StorageService.to.markBindVehicleDialogAsShown();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,6 +770,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
void onClose() {
|
void onClose() {
|
||||||
amountController.dispose();
|
amountController.dispose();
|
||||||
plateNumberController.dispose();
|
plateNumberController.dispose();
|
||||||
|
if (_debounce != null) {
|
||||||
|
_debounce?.cancel();
|
||||||
|
}
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:getx_scaffold/common/index.dart';
|
||||||
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
|
import 'package:ln_jq_app/pages/c_page/reservation/controller.dart';
|
||||||
|
import 'package:ln_jq_app/pages/c_page/reservation_edit/controller.dart';
|
||||||
|
import 'package:ln_jq_app/pages/c_page/reservation_edit/view.dart';
|
||||||
|
|
||||||
|
//加氢预约列表
|
||||||
|
class ReservationListBottomSheet extends StatefulWidget {
|
||||||
|
const ReservationListBottomSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ReservationListBottomSheet> createState() => _ReservationListBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReservationListBottomSheetState extends State<ReservationListBottomSheet> {
|
||||||
|
final C_ReservationController _controller = Get.find<C_ReservationController>();
|
||||||
|
|
||||||
|
final Map<String, String> _statusOptions = {
|
||||||
|
'': '所有状态', // 增加一个“所有状态”的选项
|
||||||
|
'0': '待加氢',
|
||||||
|
'1': '加氢完成',
|
||||||
|
'2': '未加氢',
|
||||||
|
'5': '拒绝加氢',
|
||||||
|
};
|
||||||
|
String _selectedStatus = ''; // 默认选中 '0' (待加氢)
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Widget 初始化时,立即调用接口加载默认状态(待加氢)的数据
|
||||||
|
_controller.getReservationList(addStatus: _selectedStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: Get.height * 0.55,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 构建标题和下拉框
|
||||||
|
_buildHeader(),
|
||||||
|
const Divider(height: 1),
|
||||||
|
// 下拉筛选框
|
||||||
|
_buildChoice(),
|
||||||
|
// 构建列表(使用 Obx 监听数据变化)
|
||||||
|
_buildList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Container _buildChoice() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 8, 0, 0),
|
||||||
|
alignment: AlignmentGeometry.centerLeft,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
value: _selectedStatus,
|
||||||
|
underline: const SizedBox.shrink(), // 隐藏下划线
|
||||||
|
items: _statusOptions.entries.map((entry) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: entry.key,
|
||||||
|
child: Text(entry.value),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
if (newValue != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedStatus = newValue;
|
||||||
|
});
|
||||||
|
// 当选择新状态时,调用接口刷新数据
|
||||||
|
_controller.getReservationList(addStatus: _selectedStatus);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建标题、关闭按钮和下拉筛选框
|
||||||
|
Widget _buildHeader() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 8, 8, 8),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Center(child: const Text('我的预约', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
|
),
|
||||||
|
child: const Text('关闭', style: TextStyle(color: Colors.black54)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建预约列表
|
||||||
|
Widget _buildList() {
|
||||||
|
return Expanded(
|
||||||
|
child: Obx(() {
|
||||||
|
if (!_controller.hasReservationData.value) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(top: 40),
|
||||||
|
child: TextX.bodyLarge('暂无该状态下的预约', weight: FontWeight.w500),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: _controller.reservationList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final reservation = _controller.reservationList[index];
|
||||||
|
return Card(
|
||||||
|
color: Colors.white,
|
||||||
|
margin: const EdgeInsets.only(bottom: 12.0),
|
||||||
|
elevation: 1,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 状态标签
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 5,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade50, // 淡蓝色背景
|
||||||
|
borderRadius: BorderRadius.circular(4), // 小圆角
|
||||||
|
// 可以选择去掉边框,或者用极淡的边框
|
||||||
|
border: Border.all(color: Colors.blue.shade100),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"${reservation.stateName}-${reservation.addStatusName}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue.shade700,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Spacer(),
|
||||||
|
//取消预约
|
||||||
|
if (reservation.hasEdit)
|
||||||
|
SizedBox(
|
||||||
|
height: 28, // 限制按钮高度,显得精致
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () async{
|
||||||
|
var responseData = await HttpService.to.post(
|
||||||
|
'appointment/orderAddHyd/vehicle-cancel',
|
||||||
|
data: {
|
||||||
|
'id': reservation.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (responseData == null || responseData.data == null) {
|
||||||
|
showToast('服务暂不可用,请稍后');
|
||||||
|
dismissLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
|
|
||||||
|
if (result.code == 0) {
|
||||||
|
showSuccessToast("已取消");
|
||||||
|
_controller.getReservationList(
|
||||||
|
addStatus: _selectedStatus,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showToast(result.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
side: BorderSide(color: Colors.grey.shade400), // 灰色边框
|
||||||
|
shape: const StadiumBorder(), // 胶囊形状
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'取消预约',
|
||||||
|
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w,),
|
||||||
|
// 修改按钮 (仅在 hasEdit 为 true 时显示)
|
||||||
|
if (reservation.hasEdit)
|
||||||
|
SizedBox(
|
||||||
|
height: 28,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () async{
|
||||||
|
var result = await Get.to(
|
||||||
|
() => ReservationEditPage(),
|
||||||
|
arguments: {
|
||||||
|
'reservation': reservation,
|
||||||
|
'difference': _controller.difference,
|
||||||
|
},
|
||||||
|
binding: BindingsBuilder(() {
|
||||||
|
Get.put(ReservationEditController());
|
||||||
|
}),
|
||||||
|
preventDuplicates: false,
|
||||||
|
);
|
||||||
|
if (result == true) {
|
||||||
|
_controller.getReservationList(
|
||||||
|
addStatus: _selectedStatus,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
side: const BorderSide(color: Colors.blue), // 蓝色边框
|
||||||
|
shape: const StadiumBorder(),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'修改',
|
||||||
|
style: TextStyle(color: Colors.blue, fontSize: 12),
|
||||||
|
),
|
||||||
|
),),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildDetailRow('车牌号:', reservation.plateNumber),
|
||||||
|
_buildDetailRow('预约日期:', reservation.date),
|
||||||
|
_buildDetailRow('预约氢量:', reservation.hydAmount),
|
||||||
|
_buildDetailRow('加氢站:', reservation.stationName),
|
||||||
|
_buildDetailRow('开始时间:', reservation.startTime),
|
||||||
|
_buildDetailRow('结束时间:', reservation.endTime),
|
||||||
|
_buildDetailRow('联系人:', reservation.contacts),
|
||||||
|
_buildDetailRow('联系电话:', reservation.phone),
|
||||||
|
reservation.addStatus == "5"
|
||||||
|
? _buildDetailRow('拒绝原因:', reservation.rejectReason)
|
||||||
|
: SizedBox(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建详情行 (这是一个辅助Widget)
|
||||||
|
Widget _buildDetailRow(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(color: Colors.grey)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,11 @@ import 'package:ln_jq_app/pages/qr_code/view.dart';
|
|||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
|
import 'reservation_list_bottomsheet.dart';
|
||||||
|
|
||||||
///加氢预约
|
///加氢预约
|
||||||
class ReservationPage extends GetView<C_ReservationController> {
|
class ReservationPage extends GetView<C_ReservationController> {
|
||||||
const ReservationPage({super.key});
|
ReservationPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -201,12 +202,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,29 +259,23 @@ 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(
|
||||||
label: '车牌号',
|
label: '车牌号',
|
||||||
controller: controller.plateNumberController,
|
controller: controller.plateNumberController,
|
||||||
hint: '请输入车牌号', // 修改提示文案
|
hint: '请输入车牌号', // 修改提示文案
|
||||||
enabled: false, // 设置为不可编辑
|
enabled: false, // 设置为不可编辑
|
||||||
),
|
),*/
|
||||||
_buildStationSelector(),
|
_buildStationSelector(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Row(
|
Row(
|
||||||
@@ -310,7 +300,9 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: controller.getReservationList,
|
onPressed: () {
|
||||||
|
controller.getReservationList(showPopup: true, addStatus: '');
|
||||||
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
minimumSize: const Size(double.infinity, 38), // 高度与另一个按钮保持一致
|
minimumSize: const Size(double.infinity, 38), // 高度与另一个按钮保持一致
|
||||||
side: const BorderSide(color: Colors.blue),
|
side: const BorderSide(color: Colors.blue),
|
||||||
@@ -381,6 +373,7 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
TextInputType? keyboardType,
|
TextInputType? keyboardType,
|
||||||
bool enabled = true,
|
bool enabled = true,
|
||||||
}) {
|
}) {
|
||||||
|
bool showCounter = keyboardType == TextInputType.number;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12.0),
|
padding: const EdgeInsets.only(bottom: 12.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -404,6 +397,25 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
filled: !enabled,
|
filled: !enabled,
|
||||||
fillColor: Colors.grey[100],
|
fillColor: Colors.grey[100],
|
||||||
|
// 左侧减号按钮
|
||||||
|
prefixIcon: showCounter
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.remove, color: Colors.blue),
|
||||||
|
onPressed: () => _updateAmount(-1),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
|
||||||
|
// 右侧加号按钮
|
||||||
|
suffixIcon: showCounter
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.add, color: Colors.blue),
|
||||||
|
onPressed: () => _updateAmount(1),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -411,6 +423,37 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// :更新氢量逻辑
|
||||||
|
void _updateAmount(int change) {
|
||||||
|
// 获取当前输入框的值,默认为 0
|
||||||
|
double currentAmount = double.tryParse(controller.amountController.text) ?? 0;
|
||||||
|
|
||||||
|
// 获取最大值,注意处理 difference 可能为空或非数字的情况
|
||||||
|
double maxAmount = double.tryParse(controller.difference.toString()) ?? 9999;
|
||||||
|
|
||||||
|
// 计算新值
|
||||||
|
double newAmount = currentAmount + change;
|
||||||
|
|
||||||
|
// 边界检查
|
||||||
|
if (newAmount < 1) {
|
||||||
|
newAmount = 1; // 最小不能小于 1
|
||||||
|
} else if (newAmount > maxAmount) {
|
||||||
|
newAmount = maxAmount; // 最大不能超过 difference
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是整数,去掉小数点显示
|
||||||
|
if (newAmount == newAmount.toInt()) {
|
||||||
|
controller.amountController.text = newAmount.toInt().toString();
|
||||||
|
} else {
|
||||||
|
controller.amountController.text = newAmount.toStringAsFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动光标到末尾,防止光标跳到前面
|
||||||
|
controller.amountController.selection = TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: controller.amountController.text.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStationSelector() {
|
Widget _buildStationSelector() {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -443,24 +486,45 @@ 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
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
value: controller.selectedStationId.value,
|
value:
|
||||||
|
// 当前的站点 处理默认
|
||||||
|
controller.selectedStationId.value ??
|
||||||
|
(controller.stationOptions.isNotEmpty
|
||||||
|
? controller.stationOptions.first.hydrogenId
|
||||||
|
: null),
|
||||||
// 当前选中的是站点ID
|
// 当前选中的是站点ID
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
controller.selectedStationId.value = value;
|
controller.selectedStationId.value = value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
customButton: controller.selectedStationId.value == null
|
customButton: Obx(() {
|
||||||
? null // 未选择时,显示默认的 hint
|
// 优先从已选中的 ID 查找
|
||||||
: _buildSelectedStationButton(
|
var selectedStation = controller.stationOptions.firstWhereOrNull(
|
||||||
controller.stationOptions.firstWhere(
|
|
||||||
(s) => s.hydrogenId == controller.selectedStationId.value,
|
(s) => s.hydrogenId == controller.selectedStationId.value,
|
||||||
),
|
);
|
||||||
),
|
|
||||||
|
// 如果找不到已选中的(比如 ID 为空或列表里没有),并且列表不为空,则取第一个作为默认
|
||||||
|
final stationToShow =
|
||||||
|
selectedStation ??
|
||||||
|
(controller.stationOptions.isNotEmpty
|
||||||
|
? controller.stationOptions.first
|
||||||
|
: null);
|
||||||
|
|
||||||
|
// 如果有要显示的站点,就构建按钮
|
||||||
|
if (stationToShow != null) {
|
||||||
|
return _buildSelectedStationButton(stationToShow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则,返回一个空占位符,让 hint 生效
|
||||||
|
// DropdownButton2 内部会判断,如果 customButton 返回的不是一个有效Widget(或根据其内部逻辑),就会显示 hint
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}),
|
||||||
buttonStyleData: ButtonStyleData(
|
buttonStyleData: ButtonStyleData(
|
||||||
height: 40, // 增加高度以容纳两行文字
|
height: 40, // 增加高度以容纳两行文字
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
@@ -489,7 +553,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 +573,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)
|
||||||
|
|||||||
267
ln_jq_app/lib/pages/c_page/reservation_edit/controller.dart
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
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 < 24; i++) {
|
||||||
|
// 每次增加 60 分钟
|
||||||
|
final startMinutes = i * 60;
|
||||||
|
final endMinutes = startMinutes + 60;
|
||||||
|
|
||||||
|
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
|
||||||
|
// 注意:endMinutes % 60 始终为 0,因为间隔是整小时
|
||||||
|
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
|
||||||
|
|
||||||
|
// 如果不是今天,所有时间段都有效
|
||||||
|
if (!isToday) {
|
||||||
|
availableSlots.add(TimeSlot(startTime, endTime));
|
||||||
|
} else {
|
||||||
|
// 如果是今天,需要判断该时间段是否可选
|
||||||
|
// 创建时间段的结束时间对象
|
||||||
|
final slotEndDateTime = DateTime(
|
||||||
|
selectedDate.value.year,
|
||||||
|
selectedDate.value.month,
|
||||||
|
selectedDate.value.day,
|
||||||
|
endTime.hour,
|
||||||
|
endTime.minute,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 注意:如果是跨天的 00:00 (例如 23:00 - 00:00),需要将日期加一天,否则 isAfter 判断会出错
|
||||||
|
// 但由于我们用的是 endTime.hour % 24,当变成 0 时,日期还是 selectedDate
|
||||||
|
// 这里做一个特殊处理:如果 endTime 是 00:00,意味着它实际上是明天的开始
|
||||||
|
DateTime realEndDateTime = slotEndDateTime;
|
||||||
|
if (endTime.hour == 0 && endTime.minute == 0) {
|
||||||
|
realEndDateTime = slotEndDateTime.add(const Duration(days: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只要时间段的结束时间晚于当前时间,这个时间段就是可预约的
|
||||||
|
if (realEndDateTime.isAfter(now)) {
|
||||||
|
availableSlots.add(TimeSlot(startTime, endTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableSlots.isEmpty) {
|
||||||
|
showToast('今天已没有可预约的时间段');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找当前选中的时间对应的新列表中的索引
|
||||||
|
int initialItem = availableSlots.indexWhere(
|
||||||
|
(slot) => slot.start.hour == startTime.value.hour,
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
showToast(result.error);
|
||||||
|
}
|
||||||
|
} 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
@@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart';
|
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
|
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
|
||||||
import 'package:ln_jq_app/pages/login/view.dart';
|
import 'package:ln_jq_app/pages/login/view.dart';
|
||||||
@@ -12,14 +17,19 @@ class HomeController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
HomeController();
|
HomeController();
|
||||||
|
|
||||||
|
final _aliyunPush = AliyunPushFlutter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
initAliyunPush();
|
||||||
|
addPushCallback();
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据登录状态和登录渠道返回不同的首页
|
// 根据登录状态和登录渠道返回不同的首页
|
||||||
Widget getHomePage() {
|
Widget getHomePage() {
|
||||||
|
requestPermission();
|
||||||
//登录状态跳转
|
//登录状态跳转
|
||||||
if (StorageService.to.isLoggedIn) {
|
if (StorageService.to.isLoggedIn) {
|
||||||
// 如果已登录,再判断是哪个渠道
|
// 如果已登录,再判断是哪个渠道
|
||||||
@@ -35,4 +45,132 @@ class HomeController extends GetxController with BaseControllerMixin {
|
|||||||
return LoginPage();
|
return LoginPage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void requestPermission() async {
|
||||||
|
PermissionStatus status = await Permission.notification.status;
|
||||||
|
if (status.isGranted) {
|
||||||
|
Logger.d("通知权限已开启");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.isDenied) {
|
||||||
|
// 建议此处增加一个应用内的 Rationale (解释说明) 弹窗
|
||||||
|
status = await Permission.notification.request();
|
||||||
|
}
|
||||||
|
if (status.isGranted) {
|
||||||
|
// 授权成功
|
||||||
|
Logger.d('通知已开启');
|
||||||
|
} else if (status.isPermanentlyDenied) {
|
||||||
|
Logger.d('通知权限已被拒绝,请到系统设置中开启');
|
||||||
|
} else if (status.isDenied) {
|
||||||
|
Logger.d('请授予通知权限,以便接收加氢站通知');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///推送相关
|
||||||
|
Future<void> initAliyunPush() async {
|
||||||
|
// 1. 配置分离:建议将 Key 提取到外部或配置文件中
|
||||||
|
final String appKey = Platform.isIOS ? AppTheme.ios_key : AppTheme.android_key;
|
||||||
|
final String appSecret = Platform.isIOS
|
||||||
|
? AppTheme.ios_appsecret
|
||||||
|
: AppTheme.android_appsecret;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 初始化推送
|
||||||
|
final result = await _aliyunPush.initPush(appKey: appKey, appSecret: appSecret);
|
||||||
|
|
||||||
|
if (result['code'] != kAliyunPushSuccessCode) {
|
||||||
|
Logger.d('初始化推送失败: ${result['code']} - ${result['errorMsg']}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.d('阿里云推送初始化成功');
|
||||||
|
// 分平台配置
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
await _setupIOSConfig();
|
||||||
|
} else if (Platform.isAndroid) {
|
||||||
|
await _setupAndroidConfig();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.d('初始化过程中发生异常: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// iOS 专属配置
|
||||||
|
Future<void> _setupIOSConfig() async {
|
||||||
|
final res = await _aliyunPush.showIOSNoticeWhenForeground(true);
|
||||||
|
if (res['code'] == kAliyunPushSuccessCode) {
|
||||||
|
Logger.d('iOS 前台通知展示已开启');
|
||||||
|
} else {
|
||||||
|
Logger.d('iOS 前台通知开启失败: ${res['errorMsg']}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Android 专属配置
|
||||||
|
Future<void> _setupAndroidConfig() async {
|
||||||
|
await _aliyunPush.setNotificationInGroup(true);
|
||||||
|
final res = await _aliyunPush.createAndroidChannel(
|
||||||
|
"xll_push_android",
|
||||||
|
'新消息通知',
|
||||||
|
4,
|
||||||
|
'用于接收加氢站实时状态提醒',
|
||||||
|
);
|
||||||
|
if (res['code'] == kAliyunPushSuccessCode) {
|
||||||
|
Logger.d('Android 通知通道创建成功');
|
||||||
|
} else {
|
||||||
|
Logger.d('Android 通道创建失败: ${res['code']} - ${res['errorMsg']}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addPushCallback() {
|
||||||
|
_aliyunPush.addMessageReceiver(
|
||||||
|
onNotification: _onNotification,
|
||||||
|
onNotificationOpened: _onNotificationOpened,
|
||||||
|
onNotificationRemoved: _onNotificationRemoved,
|
||||||
|
onMessage: _onMessage,
|
||||||
|
onAndroidNotificationReceivedInApp: _onAndroidNotificationReceivedInApp,
|
||||||
|
onAndroidNotificationClickedWithNoAction: _onAndroidNotificationClickedWithNoAction,
|
||||||
|
onIOSChannelOpened: _onIOSChannelOpened,
|
||||||
|
onIOSRegisterDeviceTokenSuccess: _onIOSRegisterDeviceTokenSuccess,
|
||||||
|
onIOSRegisterDeviceTokenFailed: _onIOSRegisterDeviceTokenFailed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onAndroidNotificationClickedWithNoAction(
|
||||||
|
Map<dynamic, dynamic> message,
|
||||||
|
) async {
|
||||||
|
Logger.d('onAndroidNotificationClickedWithNoAction ====> $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onAndroidNotificationReceivedInApp(Map<dynamic, dynamic> message) async {
|
||||||
|
Logger.d('onAndroidNotificationReceivedInApp ====> $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onMessage(Map<dynamic, dynamic> message) async {
|
||||||
|
Logger.d('onMessage ====> $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onNotification(Map<dynamic, dynamic> message) async {
|
||||||
|
Logger.d('onNotification ====> $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onNotificationOpened(Map<dynamic, dynamic> message) async {
|
||||||
|
Logger.d('onNotificationOpened ====> $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onNotificationRemoved(Map<dynamic, dynamic> message) async {
|
||||||
|
Logger.d('onNotificationRemoved ====> $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onIOSChannelOpened(Map<dynamic, dynamic> message) async {
|
||||||
|
Logger.d('onIOSChannelOpened ====> $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onIOSRegisterDeviceTokenSuccess(Map<dynamic, dynamic> message) async {
|
||||||
|
Logger.d('onIOSRegisterDeviceTokenSuccess ====> $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onIOSRegisterDeviceTokenFailed(Map<dynamic, dynamic> message) async {
|
||||||
|
Logger.d('onIOSRegisterDeviceTokenFailed====> $message');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/common/login_util.dart';
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
|
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart';
|
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
|
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
|
||||||
import 'package:ln_jq_app/pages/login/controller.dart';
|
import 'package:ln_jq_app/pages/login/controller.dart';
|
||||||
|
import 'package:ln_jq_app/pages/url_host/view.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
@@ -20,8 +25,10 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
|
|
||||||
bool cLogin = true;
|
bool cLogin = true;
|
||||||
bool _obscureText = true;
|
bool _obscureText = true;
|
||||||
|
|
||||||
// 用于管理“记住密码”的复选框状态
|
// 用于管理“记住密码”的复选框状态
|
||||||
bool _rememberPassword = true;
|
bool _rememberPassword = true;
|
||||||
|
|
||||||
// 用于确保凭证只在首次加载时回填一次
|
// 用于确保凭证只在首次加载时回填一次
|
||||||
bool _credentialsLoaded = false;
|
bool _credentialsLoaded = false;
|
||||||
|
|
||||||
@@ -174,8 +181,16 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
showToast('登录失败:无法获取凭证');
|
showToast('登录失败:无法获取凭证');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
//登录信息处理
|
||||||
try {
|
try {
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
|
|
||||||
|
if (result.code != 0) {
|
||||||
|
showToast(result.error);
|
||||||
|
dismissLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String token = result.data['token'] ?? '';
|
String token = result.data['token'] ?? '';
|
||||||
String idCard = result.data['idCard'] ?? '';
|
String idCard = result.data['idCard'] ?? '';
|
||||||
String name = result.data['name'] ?? '';
|
String name = result.data['name'] ?? '';
|
||||||
@@ -188,6 +203,26 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
name: name,
|
name: name,
|
||||||
phone: phone,
|
phone: phone,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//注册推送别名
|
||||||
|
addAlias(phone);
|
||||||
|
|
||||||
|
//登录后查询已绑定车辆信息
|
||||||
|
var carInfo = await HttpService.to.get(
|
||||||
|
"appointment/driver/getTruckInfoByDriver?phone=$phone",
|
||||||
|
);
|
||||||
|
if (carInfo != null) {
|
||||||
|
var carInforesult = BaseModel.fromJson(carInfo.data);
|
||||||
|
if (carInforesult.data != null) {
|
||||||
|
final vehicle = VehicleInfo.fromJson(
|
||||||
|
carInforesult.data as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
//保存使用
|
||||||
|
await StorageService.to.saveVehicleInfo(vehicle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//页面操作
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
showToast('登录成功,欢迎您');
|
showToast('登录成功,欢迎您');
|
||||||
Get.offAll(() => BaseWidgetsPage());
|
Get.offAll(() => BaseWidgetsPage());
|
||||||
@@ -244,9 +279,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
hintText: '请输入密码',
|
hintText: '请输入密码',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(_obscureText ? Icons.visibility_off : Icons.visibility),
|
||||||
_obscureText ? Icons.visibility_off : Icons.visibility,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_obscureText = !_obscureText;
|
_obscureText = !_obscureText;
|
||||||
@@ -279,7 +312,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => setState(() => _rememberPassword = !_rememberPassword),
|
onTap: () => setState(() => _rememberPassword = !_rememberPassword),
|
||||||
child: const Text('记住密码', style: TextStyle(color: Colors.grey)),
|
child: const Text('记住密码', style: TextStyle(color: Colors.grey)),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 20), // 调整间距
|
SizedBox(height: 20), // 调整间距
|
||||||
@@ -303,7 +336,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
try {
|
try {
|
||||||
String encryptedPassword = LoginUtil.encrypt(password);
|
String encryptedPassword = LoginUtil.encrypt(password);
|
||||||
var responseData = await HttpService.to.post(
|
var responseData = await HttpService.to.post(
|
||||||
'/login/password',
|
'appointment/login/password',
|
||||||
data: {
|
data: {
|
||||||
'account': account,
|
'account': account,
|
||||||
'password': encryptedPassword,
|
'password': encryptedPassword,
|
||||||
@@ -319,15 +352,27 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
|
|
||||||
|
if (result.code != 0) {
|
||||||
|
showToast(result.error);
|
||||||
|
dismissLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String token = result.data['token'] ?? '';
|
String token = result.data['token'] ?? '';
|
||||||
String userId = result.data['userId'] ?? '';
|
String userId = result.data['userId'] ?? '';
|
||||||
|
String mobile = result.data['mobile'] ?? '';
|
||||||
|
|
||||||
await StorageService.to.saveLoginInfo(
|
await StorageService.to.saveLoginInfo(
|
||||||
token: token,
|
token: token,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
phone: mobile,
|
||||||
channel: "station",
|
channel: "station",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//注册推送别名
|
||||||
|
addAlias(mobile);
|
||||||
|
|
||||||
// 根据复选框状态保存或清除密码 ---
|
// 根据复选框状态保存或清除密码 ---
|
||||||
if (_rememberPassword) {
|
if (_rememberPassword) {
|
||||||
await StorageService.to.saveStationCredentials(account, password);
|
await StorageService.to.saveStationCredentials(account, password);
|
||||||
@@ -352,13 +397,51 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _aliyunPush = AliyunPushFlutter();
|
||||||
|
|
||||||
|
void addAlias(String alias) async {
|
||||||
|
var result = await _aliyunPush.addAlias(alias);
|
||||||
|
var code = result['code'];
|
||||||
|
if (code == kAliyunPushSuccessCode) {
|
||||||
|
Logger.d('添加别名$alias成功');
|
||||||
|
} else {
|
||||||
|
var errorCode = result['code'];
|
||||||
|
var errorMsg = result['errorMsg'];
|
||||||
|
Logger.d('添加别名$alias失败: $errorCode - $errorMsg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<LoginController>(
|
return GetBuilder<LoginController>(
|
||||||
init: LoginController(),
|
init: LoginController(),
|
||||||
id: 'login',
|
id: 'login',
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
return Scaffold(body: _buildView(controller));
|
return Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: _buildView(controller)),
|
||||||
|
if (AppTheme.is_show_host)
|
||||||
|
Positioned(
|
||||||
|
top: 40.h,
|
||||||
|
right: 20.w,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.to(() => const UrlHostPage());
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"域名配置",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:image/image.dart' as img;
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
import 'package:zxing_lib/common.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:zxing_lib/qrcode.dart';
|
|
||||||
import 'package:zxing_lib/zxing.dart';
|
|
||||||
|
|
||||||
class QrCodeController extends GetxController
|
class QrCodeController extends GetxController
|
||||||
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
|
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
|
||||||
@@ -23,11 +18,13 @@ class QrCodeController extends GetxController
|
|||||||
late final AnimationController animationController;
|
late final AnimationController animationController;
|
||||||
late final Animation<double> scanAnimation;
|
late final Animation<double> scanAnimation;
|
||||||
|
|
||||||
// --- QR Scanning ---
|
// --- 使用 MobileScanner 的控制器 ---
|
||||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
final MobileScannerController scannerController = MobileScannerController();
|
||||||
QRViewController? qrViewController;
|
|
||||||
final Rx<Barcode?> result = Rx<Barcode?>(null);
|
|
||||||
final RxBool isFlashOn = false.obs;
|
final RxBool isFlashOn = false.obs;
|
||||||
|
final RxBool isProcessingResult = false.obs;
|
||||||
|
|
||||||
|
final RxBool hasPermission = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@@ -38,171 +35,214 @@ class QrCodeController extends GetxController
|
|||||||
duration: const Duration(milliseconds: 2500),
|
duration: const Duration(milliseconds: 2500),
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
scanAnimation = Tween<double>(begin: 0, end: 1).animate(animationController);
|
scanAnimation =
|
||||||
|
Tween<double>(begin: 0, end: 1).animate(animationController);
|
||||||
animationController.repeat(reverse: false);
|
animationController.repeat(reverse: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当 QRView 创建时调用
|
/// MobileScanner 的 onDetect 回调方法
|
||||||
void onQRViewCreated(QRViewController controller) {
|
void onDetect(BarcodeCapture capture) {
|
||||||
this.qrViewController = controller;
|
if (isProcessingResult.value) return;
|
||||||
// 监听扫描到的数据
|
|
||||||
controller.scannedDataStream.listen((scanData) {
|
|
||||||
if (scanData.code != null && result.value?.code != scanData.code) {
|
|
||||||
result.value = scanData;
|
|
||||||
qrViewController?.pauseCamera();
|
|
||||||
|
|
||||||
|
final Barcode? barcode = capture.barcodes.firstOrNull;
|
||||||
|
if (barcode?.rawValue != null && barcode!.rawValue!.isNotEmpty) {
|
||||||
|
isProcessingResult.value = true;
|
||||||
|
scannerController.stop();
|
||||||
animationController.stop();
|
animationController.stop();
|
||||||
|
renderResult(barcode.rawValue!);
|
||||||
renderResult(scanData.code!);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 恢复扫描状态
|
||||||
void resumeScanner() {
|
void resumeScanner() {
|
||||||
result.value = null;
|
isProcessingResult.value = false;
|
||||||
qrViewController?.resumeCamera();
|
try {
|
||||||
|
scannerController.start();
|
||||||
animationController.repeat(reverse: false);
|
animationController.repeat(reverse: false);
|
||||||
|
} catch (e) {
|
||||||
|
print("无法重启相机: $e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从相册选择图片并扫描二维码
|
/// 从相册选择图片并扫描二维码
|
||||||
void scanFromGallery() async {
|
void scanFromGallery() async {
|
||||||
try {
|
try {
|
||||||
final XFile? imageFile = await ImagePicker().pickImage(source: ImageSource.gallery);
|
final XFile? imageFile =
|
||||||
if (imageFile == null) return; // 用户取消了选择
|
await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||||
|
if (imageFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
qrViewController?.pauseCamera();
|
scannerController.stop();
|
||||||
animationController.stop();
|
animationController.stop();
|
||||||
|
showLoading("正在识别...");
|
||||||
|
|
||||||
String? scanResult;
|
final BarcodeCapture? capture =
|
||||||
try {
|
await scannerController.analyzeImage(imageFile.path);
|
||||||
final image = img.decodeImage(await File(imageFile.path).readAsBytes());
|
|
||||||
if (image != null) {
|
|
||||||
//扫描图片
|
|
||||||
final pixels = Int32List.fromList(
|
|
||||||
image.map((pixel) {
|
|
||||||
return (pixel.a.toInt() << 24) |
|
|
||||||
(pixel.r.toInt() << 16) |
|
|
||||||
(pixel.g.toInt() << 8) |
|
|
||||||
pixel.b.toInt();
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final source = RGBLuminanceSource(image.width, image.height, pixels);
|
dismissLoading();
|
||||||
|
|
||||||
final bitmap = BinaryBitmap(HybridBinarizer(source));
|
final Barcode? firstBarcode = capture?.barcodes.firstOrNull;
|
||||||
final reader = QRCodeReader();
|
|
||||||
final result = reader.decode(bitmap);
|
|
||||||
scanResult = result.text;
|
|
||||||
}
|
|
||||||
} on NotFoundException {
|
|
||||||
scanResult = null;
|
|
||||||
} catch (e) {
|
|
||||||
//异常
|
|
||||||
scanResult = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scanResult != null) {
|
if (firstBarcode?.rawValue != null &&
|
||||||
|
firstBarcode!.rawValue!.isNotEmpty) {
|
||||||
|
final String scanResult = firstBarcode.rawValue!;
|
||||||
|
print("相册识别到的内容: $scanResult");
|
||||||
renderResult(scanResult);
|
renderResult(scanResult);
|
||||||
} else {
|
} else {
|
||||||
showErrorToast('未识别到二维码');
|
showErrorToast('未识别到二维码');
|
||||||
resumeScanner();
|
resumeScanner();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, stackTrace) {
|
||||||
showErrorToast('从相册选择失败');
|
dismissLoading();
|
||||||
|
showErrorToast('从相册选择失败,请稍后重试');
|
||||||
|
print("scanFromGallery Error: $e\n$stackTrace");
|
||||||
resumeScanner();
|
resumeScanner();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 切换闪光灯
|
/// 切换闪光灯
|
||||||
void toggleFlash() async {
|
void toggleFlash() async {
|
||||||
await qrViewController?.toggleFlash();
|
try {
|
||||||
isFlashOn.value = (await qrViewController?.getFlashStatus()) ?? false;
|
await scannerController.toggleTorch();
|
||||||
|
final currentTorchState = scannerController.value.torchState;
|
||||||
|
isFlashOn.value = currentTorchState == TorchState.on;
|
||||||
|
} catch (e) {
|
||||||
|
print("切换闪光灯失败: $e");
|
||||||
|
showErrorToast("无法打开闪光灯");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 翻转相机
|
/// 翻转相机
|
||||||
void flipCamera() async {
|
void flipCamera() async {
|
||||||
await qrViewController?.flipCamera();
|
await scannerController.switchCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 请求相机权限
|
||||||
void requestPermission() async {
|
void requestPermission() async {
|
||||||
if(Platform.isIOS){
|
|
||||||
var status = await Permission.camera.request();
|
var status = await Permission.camera.request();
|
||||||
if (status.isGranted) {
|
|
||||||
|
hasPermission.value = status.isGranted;
|
||||||
|
|
||||||
|
if (!status.isGranted) {
|
||||||
|
if (status.isPermanentlyDenied) {
|
||||||
|
showErrorToast('相机权限已被永久拒绝,请到系统设置中开启');
|
||||||
|
// 延迟一会再引导用户去设置
|
||||||
|
Future.delayed(const Duration(seconds: 2), () => openAppSettings());
|
||||||
|
} else {
|
||||||
|
showErrorToast('请授予相机权限以使用扫描功能');
|
||||||
}
|
}
|
||||||
else if (status.isPermanentlyDenied) {
|
|
||||||
openAppSettings();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
showErrorToast('需要相机权限才能扫描二维码');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool results = await requestCameraPermission();
|
|
||||||
if (!results) {
|
|
||||||
showErrorToast('相机权限未被授予,请到权限管理中打开');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void requestPhotoPermission() async {
|
void requestPhotoPermission() async {
|
||||||
if (Platform.isAndroid) {
|
|
||||||
final bool results = await requestPhotosPermission();
|
|
||||||
if (!results) {
|
|
||||||
showErrorToast('相册权限未被授予,请到权限管理中打开');
|
|
||||||
} else {
|
|
||||||
scanFromGallery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(Platform.isIOS){
|
|
||||||
var status = await Permission.photos.request();
|
var status = await Permission.photos.request();
|
||||||
print("权限状态: $status"); // 在控制台看这个输出
|
|
||||||
if (status.isGranted) {
|
if (status.isGranted) {
|
||||||
scanFromGallery();
|
scanFromGallery();
|
||||||
}
|
} else if (status.isPermanentlyDenied) {
|
||||||
else if (status.isPermanentlyDenied) {
|
|
||||||
openAppSettings();
|
openAppSettings();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
showErrorToast('需要相册权限才能从相册中选择图片');
|
showErrorToast('需要相册权限才能从相册中选择图片');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
//扫码结果处理
|
/// 处理扫描结果
|
||||||
void renderResult(String resultStr) async {
|
void renderResult(String resultStr, {plateNumber}) async {
|
||||||
showLoading("正在获取车辆信息...");
|
showLoading("正在获取车辆信息...");
|
||||||
try {
|
try {
|
||||||
var responseData = await HttpService.to.get(
|
final Map<String, dynamic> requestData = {
|
||||||
"appointment/truck/base-info?vin=$resultStr",
|
"code": resultStr,
|
||||||
|
"phone": StorageService.to.phone,
|
||||||
|
};
|
||||||
|
if (plateNumber != null && plateNumber.isNotEmpty) {
|
||||||
|
requestData['plateNumber'] = plateNumber;
|
||||||
|
}
|
||||||
|
var responseData = await HttpService.to.post(
|
||||||
|
"appointment/truck/bindTruck",
|
||||||
|
data: requestData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (responseData == null || responseData.data == null) {
|
if (responseData == null) {
|
||||||
|
dismissLoading();
|
||||||
showToast('无法获取车辆信息,请检查网络或稍后重试');
|
showToast('无法获取车辆信息,请检查网络或稍后重试');
|
||||||
resumeScanner();
|
resumeScanner();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
final vehicle = VehicleInfo.fromJson(result.data as Map<String, dynamic>);
|
|
||||||
//保存使用
|
|
||||||
await StorageService.to.saveVehicleInfo(vehicle);
|
|
||||||
|
|
||||||
|
if (result.code != 0) {
|
||||||
|
showToast(result.error);
|
||||||
|
dismissLoading();
|
||||||
|
resumeScanner(); // 绑定失败也要恢复扫描
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data == null) {
|
||||||
|
dismissLoading();
|
||||||
|
showBindDialog(resultStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final vehicle = VehicleInfo.fromJson(result.data as Map<String, dynamic>);
|
||||||
|
await StorageService.to.saveVehicleInfo(vehicle);
|
||||||
|
dismissLoading();
|
||||||
Get.back(result: true);
|
Get.back(result: true);
|
||||||
} on DioException catch (e) {
|
|
||||||
|
} on DioException catch (_) {
|
||||||
showErrorToast("网络请求失败,请稍后重试");
|
showErrorToast("网络请求失败,请稍后重试");
|
||||||
resumeScanner();
|
resumeScanner();
|
||||||
} catch (e, stackTrace) {
|
} catch (e, _) {
|
||||||
showErrorToast("处理失败,请稍后重试");
|
showErrorToast("处理失败,请稍后重舍");
|
||||||
resumeScanner(); // 未知异常,恢复扫描
|
resumeScanner();
|
||||||
} finally {
|
} finally {
|
||||||
|
if (Get.isDialogOpen ?? false) {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 显示绑定确认对话框
|
||||||
|
void showBindDialog(String resultStr) {
|
||||||
|
final TextEditingController plateNumberController = TextEditingController();
|
||||||
|
// 使用 showConfirmDialog,它有 onCancel 回调
|
||||||
|
DialogX.to.showConfirmDialog(
|
||||||
|
title: '请输入车牌号',
|
||||||
|
barrierDismissible: false,
|
||||||
|
content: TextField(
|
||||||
|
controller: plateNumberController,
|
||||||
|
autofocus: false,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: '请输入完整的车牌号',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
confirmText: '确认绑定',
|
||||||
|
cancelText: '取消', // showConfirmDialog 有 cancelText
|
||||||
|
onConfirm: () {
|
||||||
|
final String plateNumber = plateNumberController.text.trim();
|
||||||
|
if (plateNumber.isEmpty) {
|
||||||
|
showToast("请输入车牌号");
|
||||||
|
// 返回 false 可以阻止弹窗关闭,让用户继续输入
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
renderResult(resultStr, plateNumber: plateNumber);
|
||||||
|
//关闭弹窗
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onCancel: () {
|
||||||
|
// 如果用户点击取消,恢复扫描
|
||||||
|
resumeScanner();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
qrViewController?.dispose();
|
scannerController.dispose();
|
||||||
animationController.dispose();
|
animationController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
|
|
||||||
class QrCodePage extends GetView<QrCodeController> {
|
class QrCodePage extends GetView<QrCodeController> {
|
||||||
const QrCodePage({Key? key}) : super(key: key);
|
const QrCodePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<QrCodeController>(
|
Get.put(QrCodeController());
|
||||||
init: QrCodeController(),
|
|
||||||
id: 'qrcode',
|
|
||||||
builder: (_) {
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -28,85 +25,139 @@ class QrCodePage extends GetView<QrCodeController> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: controller.requestPhotoPermission,
|
onPressed: controller.requestPhotoPermission,
|
||||||
child: const Text(
|
child: const Text('相册', style: TextStyle(color: Colors.white, fontSize: 16)),
|
||||||
'相册',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Obx(() { // 1. 使用 Obx 包裹整个 body
|
||||||
children: <Widget>[
|
// 根据权限状态来决定显示什么
|
||||||
_buildQrView(context),
|
if (controller.hasPermission.value) {
|
||||||
Positioned(
|
// 如果有权限,显示扫描器
|
||||||
bottom: 80.h,
|
return _buildScannerView(context);
|
||||||
left: 0,
|
} else {
|
||||||
right: 0,
|
// 如果没有权限,显示引导界面
|
||||||
child: _buildControlButtons(),
|
return _buildPermissionDeniedView();
|
||||||
),
|
}
|
||||||
],
|
}),
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Widget _buildScannerView(BuildContext context){
|
||||||
/// 构建二维码扫描视图(带动画)
|
if (!controller.animationController.isAnimating) {
|
||||||
Widget _buildQrView(BuildContext context) {
|
controller.animationController.repeat(reverse: false);
|
||||||
// 定义扫描区域的大小
|
}
|
||||||
var scanArea = (MediaQuery.of(context).size.width < 400 ||
|
|
||||||
MediaQuery.of(context).size.height < 400)
|
|
||||||
? 250.0
|
|
||||||
: 300.0;
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: <Widget>[
|
children: [
|
||||||
// 底层是相机视图和半透明遮罩
|
// 使用 MobileScanner 作为扫描视图
|
||||||
QRView(
|
MobileScanner(
|
||||||
key: controller.qrKey,
|
controller: controller.scannerController,
|
||||||
onQRViewCreated: controller.onQRViewCreated,
|
onDetect: controller.onDetect,
|
||||||
overlay: QrScannerOverlayShape(
|
// 您可以自定义扫描框的样式
|
||||||
borderColor: Colors.blueAccent,
|
scanWindow: Rect.fromCenter(
|
||||||
borderRadius: 10,
|
center: Offset(
|
||||||
borderLength: 30,
|
MediaQuery.of(context).size.width / 2,
|
||||||
borderWidth: 10,
|
MediaQuery.of(context).size.height / 2 - 50,
|
||||||
cutOutSize: scanArea,
|
),
|
||||||
|
width: 250,
|
||||||
|
height: 250,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 上层是扫描动画
|
// 扫描动画和覆盖层
|
||||||
AnimatedBuilder(
|
_buildScannerOverlay(context),
|
||||||
animation: controller.scanAnimation,
|
// 底部的功能按钮
|
||||||
builder: (context, child) {
|
Positioned(bottom: 80, left: 0, right: 0, child: _buildActionButtons()),
|
||||||
return Positioned(
|
],
|
||||||
// 计算扫描框的顶部位置,以便动画从顶部开始
|
);
|
||||||
top: (MediaQuery.of(context).size.height - scanArea) / 2,
|
}
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(0, controller.scanAnimation.value * scanArea),
|
Widget _buildPermissionDeniedView() {
|
||||||
|
// 确保动画是停止的
|
||||||
|
if (controller.animationController.isAnimating) {
|
||||||
|
controller.animationController.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: Colors.black,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.no_photography, color: Colors.white70, size: 64),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
'需要相机权限',
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'请授予相机权限以使用扫码功能。',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: controller.requestPermission, // 点击按钮重新请求权限
|
||||||
|
child: const Text('授予权限'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建扫描区域的覆盖层和动画
|
||||||
|
Widget _buildScannerOverlay(BuildContext context) {
|
||||||
|
// 模拟扫描框的位置和大小
|
||||||
|
const double scanAreaSize = 250.0;
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
// 半透明的覆盖层
|
||||||
|
ColorFiltered(
|
||||||
|
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.5), BlendMode.srcOut),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Container(decoration: const BoxDecoration(color: Colors.transparent)),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: scanArea,
|
margin: const EdgeInsets.only(bottom: 100), // 微调位置
|
||||||
height: 2, // 扫描线的高度
|
width: scanAreaSize,
|
||||||
|
height: scanAreaSize,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blueAccent,
|
color: Colors.black,
|
||||||
boxShadow: [
|
borderRadius: BorderRadius.circular(12),
|
||||||
BoxShadow(
|
),
|
||||||
color: Colors.blueAccent.withOpacity(0.7),
|
),
|
||||||
blurRadius: 8,
|
|
||||||
spreadRadius: 2,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 扫描动画
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 100),
|
||||||
|
width: scanAreaSize,
|
||||||
|
height: scanAreaSize,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: controller.scanAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: ScannerAnimationPainter(
|
||||||
|
controller.scanAnimation.value,
|
||||||
|
AppTheme.themeColor,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建底部的控制按钮
|
/// 构建底部的功能按钮(闪光灯、相册)
|
||||||
Widget _buildControlButtons() {
|
Widget _buildActionButtons() {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -123,11 +174,16 @@ class QrCodePage extends GetView<QrCodeController> {
|
|||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
onPressed: controller.toggleFlash,
|
onPressed: controller.toggleFlash,
|
||||||
//闪光灯状态的变化
|
//闪光灯状态的变化
|
||||||
child: Obx(() => Icon(
|
child: Obx(
|
||||||
|
() => IconButton(
|
||||||
|
icon: Icon(
|
||||||
controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off,
|
controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 28,
|
size: 28,
|
||||||
)),
|
),
|
||||||
|
onPressed: controller.toggleFlash,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
// 翻转相机按钮
|
// 翻转相机按钮
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
@@ -144,6 +200,7 @@ class QrCodePage extends GetView<QrCodeController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Widget _buildIconButton({required VoidCallback onPressed, required Widget child}) {
|
Widget _buildIconButton({required VoidCallback onPressed, required Widget child}) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -158,3 +215,71 @@ class QrCodePage extends GetView<QrCodeController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 扫描动画的绘制器
|
||||||
|
class ScannerAnimationPainter extends CustomPainter {
|
||||||
|
final double value;
|
||||||
|
final Color borderColor;
|
||||||
|
|
||||||
|
ScannerAnimationPainter(this.value, this.borderColor);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = borderColor
|
||||||
|
..strokeWidth = 3
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
final cornerLength = 20.0;
|
||||||
|
// 绘制四个角的边框
|
||||||
|
// Top-left
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(0, cornerLength)
|
||||||
|
..lineTo(0, 0)
|
||||||
|
..lineTo(cornerLength, 0),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
// Top-right
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(size.width - cornerLength, 0)
|
||||||
|
..lineTo(size.width, 0)
|
||||||
|
..lineTo(size.width, cornerLength),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
// Bottom-left
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(0, size.height - cornerLength)
|
||||||
|
..lineTo(0, size.height)
|
||||||
|
..lineTo(cornerLength, size.height),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
// Bottom-right
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(size.width - cornerLength, size.height)
|
||||||
|
..lineTo(size.width, size.height)
|
||||||
|
..lineTo(size.width, size.height - cornerLength),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制扫描线
|
||||||
|
final linePaint = Paint()
|
||||||
|
..color = borderColor.withOpacity(0.8)
|
||||||
|
..strokeWidth = 2
|
||||||
|
..shader = LinearGradient(
|
||||||
|
colors: [borderColor.withOpacity(0), borderColor, borderColor.withOpacity(0)],
|
||||||
|
stops: const [0.0, 0.5, 1.0],
|
||||||
|
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||||
|
|
||||||
|
final y = size.height * value;
|
||||||
|
canvas.drawLine(Offset(0, y), Offset(size.width, y), linePaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
65
ln_jq_app/lib/pages/url_host/controller.dart
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
|
||||||
|
class UrlHostController extends GetxController {
|
||||||
|
final TextEditingController urlController = TextEditingController();
|
||||||
|
|
||||||
|
// 预设的域名列表
|
||||||
|
final List<String> presetUrls = [
|
||||||
|
'https://beta-esg.api.lnh2e.com/', // 测试环境
|
||||||
|
'http://192.168.110.44:8080/', // 沈辰本地
|
||||||
|
'http://192.168.110.222:8080/', // 何斐本地
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<String> urlNames = [
|
||||||
|
'测试环境',
|
||||||
|
'沈辰本地环境',
|
||||||
|
'何斐本地环境',
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
// 初始化时,尝试从 StorageService 获取已保存的域名
|
||||||
|
// 如果没有保存过,则使用当前的全局配置
|
||||||
|
String? savedUrl = StorageService.to.hostUrl;
|
||||||
|
if (savedUrl != null && savedUrl.isNotEmpty) {
|
||||||
|
urlController.text = savedUrl;
|
||||||
|
} else {
|
||||||
|
urlController.text = AppTheme.test_service_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 当用户点击列表中的某一项时,将其填入编辑框
|
||||||
|
void selectUrl(String url) {
|
||||||
|
urlController.text = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存配置并关闭页面
|
||||||
|
void saveAndExit() async {
|
||||||
|
String newUrl = urlController.text.trim();
|
||||||
|
if (newUrl.isEmpty) {
|
||||||
|
showToast('请输入有效的域名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到本地存储,以便下次启动时加载
|
||||||
|
await StorageService.to.saveHostUrl(newUrl);
|
||||||
|
AppTheme.test_service_url = newUrl;
|
||||||
|
|
||||||
|
//设置框架
|
||||||
|
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
||||||
|
|
||||||
|
showSuccessToast('域名已更新:${AppTheme.test_service_url}');
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
urlController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
82
ln_jq_app/lib/pages/url_host/view.dart
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
|
||||||
|
import 'controller.dart';
|
||||||
|
|
||||||
|
class UrlHostPage extends GetView<UrlHostController> {
|
||||||
|
const UrlHostPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Get.put(UrlHostController());
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('域名配置'),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'当前环境配置',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: controller.urlController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '请输入或选择API域名',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text(
|
||||||
|
'预设环境',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: controller.presetUrls.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final url = controller.presetUrls[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 1,
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(controller.urlNames[index], style: const TextStyle(fontSize: 14)),
|
||||||
|
subtitle: Text(url, style: const TextStyle(fontSize: 14)),
|
||||||
|
trailing: const Icon(Icons.touch_app, size: 18, color: Colors.grey),
|
||||||
|
onTap: () {
|
||||||
|
// 点击列表项:先填入编辑框,然后直接保存退出
|
||||||
|
controller.selectUrl(url);
|
||||||
|
controller.saveAndExit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: controller.saveAndExit,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 48),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('保存配置', style: TextStyle(fontSize: 16)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
|
import 'package:getx_scaffold/common/utils/log_util.dart';
|
||||||
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
||||||
|
|
||||||
/// 定义登录渠道的枚举类型
|
/// 定义登录渠道的枚举类型
|
||||||
@@ -20,10 +22,13 @@ 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 const String _hostUrlKey = 'host_url';
|
||||||
|
|
||||||
static StorageService get to => Get.find();
|
static StorageService get to => Get.find();
|
||||||
|
|
||||||
@@ -33,18 +38,34 @@ class StorageService extends GetxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Getters ---
|
// --- Getters ---
|
||||||
|
String? get hostUrl => _box.read<String?>(_hostUrlKey);
|
||||||
|
|
||||||
|
///:保存自定义域名
|
||||||
|
Future<void> saveHostUrl(String url) async {
|
||||||
|
await _box.write(_hostUrlKey, url);
|
||||||
|
}
|
||||||
|
|
||||||
bool get isLoggedIn => _box.read<String?>(_tokenKey)?.isNotEmpty ?? false;
|
bool get isLoggedIn => _box.read<String?>(_tokenKey)?.isNotEmpty ?? false;
|
||||||
|
|
||||||
String? get token => _box.read<String?>(_tokenKey);
|
String? get token => _box.read<String?>(_tokenKey);
|
||||||
|
|
||||||
String? get userId => _box.read<String?>(_userIdKey);
|
String? get userId => _box.read<String?>(_userIdKey);
|
||||||
|
|
||||||
String? get name => _box.read<String?>(_nameKey);
|
String? get name => _box.read<String?>(_nameKey);
|
||||||
|
|
||||||
String? get phone => _box.read<String?>(_phoneKey);
|
String? get phone => _box.read<String?>(_phoneKey);
|
||||||
|
|
||||||
String? get idCard => _box.read<String?>(_idCardKey);
|
String? get idCard => _box.read<String?>(_idCardKey);
|
||||||
|
|
||||||
bool get hasVehicleInfo => _box.hasData(_vehicleInfoKey);
|
bool get hasVehicleInfo => _box.hasData(_vehicleInfoKey);
|
||||||
|
|
||||||
// 记住密码:获取加氢站账号和密码
|
|
||||||
String? get stationAccount => _box.read<String?>(_stationAccountKey);
|
String? get stationAccount => _box.read<String?>(_stationAccountKey);
|
||||||
|
|
||||||
String? get stationPassword => _box.read<String?>(_stationPasswordKey);
|
String? get stationPassword => _box.read<String?>(_stationPasswordKey);
|
||||||
|
|
||||||
|
// 新增:获取“绑定车辆”弹窗是否已显示的标志
|
||||||
|
bool get hasShownBindVehicleDialog => _box.read<bool>(_bindDialogShownKey) ?? false;
|
||||||
|
|
||||||
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 +105,20 @@ 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 +132,24 @@ 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);
|
||||||
|
|
||||||
|
delAlias();
|
||||||
|
}
|
||||||
|
|
||||||
|
final _aliyunPush = AliyunPushFlutter();
|
||||||
|
|
||||||
|
void delAlias() async {
|
||||||
|
String phoen = StorageService.to.phone ?? "";
|
||||||
|
var result = await _aliyunPush.removeAlias(phoen);
|
||||||
|
var code = result['code'];
|
||||||
|
if (code == kAliyunPushSuccessCode) {
|
||||||
|
Logger.d('删除别名$phoen成功');
|
||||||
|
} else {
|
||||||
|
var errorCode = result['code'];
|
||||||
|
var errorMsg = result['errorMsg'];
|
||||||
|
Logger.d('删除别名$phoen失败: $errorCode - $errorMsg');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
aliyun_push_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: aliyun_push_flutter
|
||||||
|
sha256: fd1a13ab37f30274e1eaa9c0f68001bf268f8f2e1c83730b4520aa4dff46ce70
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.6"
|
||||||
ansicolor:
|
ansicolor:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -65,14 +73,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
charset:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: charset
|
|
||||||
sha256: "27802032a581e01ac565904ece8c8962564b1070690794f0072f6865958ce8b9"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -117,18 +117,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cross_file
|
name: cross_file
|
||||||
sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239"
|
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.5"
|
version: "0.3.5+1"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.0.7"
|
||||||
csslib:
|
csslib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -261,18 +261,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_selector_linux
|
name: file_selector_linux
|
||||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+2"
|
version: "0.9.4"
|
||||||
file_selector_macos:
|
file_selector_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_selector_macos
|
name: file_selector_macos
|
||||||
sha256: "88707a3bec4b988aaed3b4df5d7441ee4e987f20b286cddca5d6a8270cab23f2"
|
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.4+5"
|
version: "0.9.5"
|
||||||
file_selector_platform_interface:
|
file_selector_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -285,10 +285,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_selector_windows
|
name: file_selector_windows
|
||||||
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+4"
|
version: "0.9.3+5"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -399,18 +399,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_pdfview
|
name: flutter_pdfview
|
||||||
sha256: a9055bf920c7095bf08c2781db431ba23577aa5da5a056a7152dc89a18fbec6f
|
sha256: c0b2cc4ebf461a5a4bb9312a165222475a7d93845c7a0703f4abb7f442eb6d54
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.4.3"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687"
|
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.32"
|
version: "2.0.33"
|
||||||
flutter_screenutil:
|
flutter_screenutil:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -431,10 +431,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
|
sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.3"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -513,10 +513,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: get
|
name: get
|
||||||
sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425
|
sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.2"
|
version: "4.7.3"
|
||||||
get_storage:
|
get_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -553,10 +553,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.6.0"
|
||||||
http_client_helper:
|
http_client_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -577,10 +577,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.4"
|
version: "4.7.2"
|
||||||
image_picker:
|
image_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -593,26 +593,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: ca2a3b04d34e76157e9ae680ef16014fb4c2d20484e78417eaed6139330056f6
|
sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.13+7"
|
version: "0.8.13+10"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_for_web
|
name: image_picker_for_web
|
||||||
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.1"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58
|
sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.13+1"
|
version: "0.8.13+3"
|
||||||
image_picker_linux:
|
image_picker_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -733,6 +733,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
mobile_scanner:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: mobile_scanner
|
||||||
|
sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "7.1.4"
|
||||||
modal_bottom_sheet:
|
modal_bottom_sheet:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -793,18 +801,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16
|
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.20"
|
version: "2.2.22"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738
|
sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.3"
|
version: "2.5.1"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -925,14 +933,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
version: "6.0.3"
|
||||||
qr_code_scanner_plus:
|
pull_to_refresh:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: qr_code_scanner_plus
|
name: pull_to_refresh
|
||||||
sha256: b764e5004251c58d9dee0c295e6006e05bd8d249e78ac3383abdb5afe0a996cd
|
sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.14"
|
version: "2.0.0"
|
||||||
rational:
|
rational:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -945,26 +953,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.3"
|
version: "2.5.4"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713"
|
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.15"
|
version: "2.4.18"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b"
|
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.5"
|
version: "2.5.6"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1070,10 +1078,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: universal_io
|
name: universal_io
|
||||||
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
|
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.2"
|
version: "2.3.1"
|
||||||
url_launcher:
|
url_launcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1086,34 +1094,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9"
|
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.24"
|
version: "6.3.28"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9"
|
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.5"
|
version: "6.3.6"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_linux
|
name: url_launcher_linux
|
||||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.2.2"
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_macos
|
name: url_launcher_macos
|
||||||
sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9"
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.4"
|
version: "3.2.5"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1134,10 +1142,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_windows
|
name: url_launcher_windows
|
||||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.5"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1234,14 +1242,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
zxing_lib:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: zxing_lib
|
|
||||||
sha256: f9170470b6bc947d21a6783486f88ef48aad66fc1380c8acd02b118418ec0ce0
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.4"
|
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.0 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
flutter: ">=3.35.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+1
|
version: 1.2.1+4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.0
|
sdk: ^3.9.0
|
||||||
@@ -43,14 +43,17 @@ dependencies:
|
|||||||
|
|
||||||
flutter_native_splash: ^2.4.7
|
flutter_native_splash: ^2.4.7
|
||||||
dropdown_button2: ^2.3.8
|
dropdown_button2: ^2.3.8
|
||||||
qr_code_scanner_plus: ^2.0.14
|
|
||||||
image_picker: ^1.2.1 # 用于从相册选择图片
|
image_picker: ^1.2.1 # 用于从相册选择图片
|
||||||
image: ^4.5.4
|
image: ^4.5.4
|
||||||
zxing_lib: ^1.1.4
|
mobile_scanner: ^7.1.4
|
||||||
flutter_pdfview: 1.3.2 #显示pdf
|
flutter_pdfview: 1.4.3 #显示pdf
|
||||||
photo_view: ^0.15.0 #操作图片
|
photo_view: ^0.15.0 #操作图片
|
||||||
flutter_inappwebview: ^6.1.5 # WebView插件
|
flutter_inappwebview: ^6.1.5 # WebView插件
|
||||||
geolocator: ^14.0.2 # 获取精确定位
|
geolocator: ^14.0.2 # 获取精确定位
|
||||||
|
aliyun_push_flutter: ^1.3.6
|
||||||
|
pull_to_refresh: ^2.0.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||