111 Commits

Author SHA1 Message Date
909dc95771 增加 提示 2026-02-11 17:41:30 +08:00
cf0896453b 样式 2026-02-11 11:28:49 +08:00
dce9718320 显示周边加氢站 2026-02-11 09:35:42 +08:00
4491aa9b91 ui调整 2026-02-10 16:35:02 +08:00
5364612a6f 更新样式 2026-02-10 13:37:24 +08:00
10867178fa 筛选框样式 2026-02-10 13:35:22 +08:00
a5e2a89e4f 预约列表样式 2026-02-10 11:51:47 +08:00
26c5f9d67a 消息样式修改 2026-02-09 17:57:00 +08:00
9cd87b0535 规则和历史 2026-02-09 17:28:12 +08:00
45e45d8160 积分兑换 2026-02-09 15:10:00 +08:00
87e890f97e 积分兑换首页 2026-02-06 17:37:43 +08:00
dcf925b8c1 商场页 2026-02-06 15:13:33 +08:00
c45863eda6 增加商城页面 2026-02-06 15:11:12 +08:00
756bf53cf5 司机预约时间调整 2026-02-06 14:16:26 +08:00
f68c2d0938 未车辆显示 2026-02-05 14:50:03 +08:00
211d0225e4 车辆图片动态 2026-02-05 13:54:10 +08:00
7d9b4d99e8 应用更新 2026-02-05 10:30:31 +08:00
3dd583a278 401增加节流 2026-02-03 10:59:05 +08:00
1278c38b7e 地图样式调整 2026-01-30 17:32:54 +08:00
171f556b40 viersion 1.2.3 2026-01-30 16:06:58 +08:00
55eade54b6 问题修改 2026-01-30 16:02:35 +08:00
bc99ffd691 增加参数 2026-01-30 13:01:50 +08:00
aa52a56bcf 地图相关的修改 2026-01-30 11:36:33 +08:00
73343ca297 调整 2026-01-30 10:01:31 +08:00
d09faac1d2 调整 2026-01-29 19:26:59 +08:00
1177be821a 样式调整 2026-01-29 17:01:21 +08:00
e59b89c225 Merge branch 'dev_feature' into dev
ui调整
# Conflicts:
#	ln_jq_app/lib/pages/b_page/reservation/controller.dart
#	ln_jq_app/lib/pages/b_page/site/controller.dart
#	ln_jq_app/lib/pages/b_page/site/view.dart
#	ln_jq_app/lib/pages/c_page/mine/view.dart
#	ln_jq_app/lib/pages/c_page/reservation/controller.dart
#	ln_jq_app/lib/pages/c_page/reservation/view.dart
#	ln_jq_app/lib/pages/login/view.dart
2026-01-29 11:45:17 +08:00
79fe3257b5 状态样式 2026-01-28 17:59:26 +08:00
55569839a7 搜索框 2026-01-28 16:27:37 +08:00
7112d70aba 站点样式 2026-01-28 15:00:30 +08:00
f8a8ecb0ed 司机 预约 样式修改 2026-01-27 17:35:37 +08:00
18c04272e2 车辆信息的部分 2026-01-27 13:57:14 +08:00
14e7fb3d78 首页ui 2026-01-27 13:15:53 +08:00
5ffaf81223 欢迎页 登录 2026-01-27 09:21:36 +08:00
907983a1d1 登录修改 2026-01-26 14:07:58 +08:00
9fdca9136d 导航栏 2026-01-23 17:00:17 +08:00
16bae6a1e9 车辆信息 ui 2026-01-23 15:02:16 +08:00
aabfbfae0c 车辆样式调整 2026-01-23 10:49:02 +08:00
5236670e7c ui修改 2026-01-23 09:22:07 +08:00
cf3ad579d3 新ui调整 2026-01-22 17:29:54 +08:00
70a752b6e5 协议同意 2026-01-21 14:29:03 +08:00
5722e3ace0 ios build 2026-01-16 15:00:45 +08:00
d41b21654a 改用账号推送 2026-01-16 14:35:11 +08:00
2eb059defd 定时器黄字2位小数 2026-01-16 13:35:38 +08:00
fbcc85af2a 站点增加消息入口 2026-01-16 13:06:35 +08:00
9a97b56505 定时器显示调整 2026-01-15 17:55:07 +08:00
8302d7c179 优化问题修改 2026-01-15 16:28:59 +08:00
e7a9e4483a 优化定时器 弹窗 2026-01-15 13:29:02 +08:00
9b64fdfa52 优化定时器 2026-01-15 13:24:02 +08:00
d8f335eb4e 优化 2026-01-15 09:30:47 +08:00
d1b7a9eb76 v1.2.2 version 2026-01-14 13:21:54 +08:00
f25feaa55a 联调修改结构 2026-01-14 13:18:22 +08:00
16639e2384 站点增加广播 2026-01-12 17:52:08 +08:00
20ef495571 非营业状态 增加时间选择 2026-01-12 16:41:36 +08:00
285a20f070 地址切换 2026-01-12 09:13:37 +08:00
baee5dba83 消息中心,待测试 2026-01-08 15:33:40 +08:00
7d9c879a4e 消息中心 2026-01-07 17:43:22 +08:00
953e5e773c 消息入口 2026-01-07 16:02:10 +08:00
8a4bc1d1ab 取消预约按钮 2026-01-07 09:59:12 +08:00
c57c849073 样式交互修改 2026-01-06 17:57:56 +08:00
6cc123f272 更换导航栏位置 2026-01-05 14:09:15 +08:00
5168b23609 增加请求头 2026-01-04 16:59:08 +08:00
c5299dd655 增加版本号 2026-01-04 10:27:39 +08:00
4bedd8c04b 优化 2025-12-31 17:37:07 +08:00
295b71c819 推送配置,测试 2025-12-31 17:22:13 +08:00
6629c8047f Merge branch 'dev' 2025-12-25 10:41:04 +08:00
bfa615a7f4 扫码无权限优化,司机预约多弹窗 2025-12-25 10:40:26 +08:00
288d629f99 bugfix 2025-12-24 10:53:34 +08:00
15fdbc7043 v1.2.1 2025-12-23 10:43:12 +08:00
f2f2348b54 逆地理编码 2025-12-23 10:18:44 +08:00
9ba152b3c3 v1.2.1 待发布 2025-12-22 16:15:32 +08:00
21a528d6d1 调整和修改 2025-12-19 13:18:04 +08:00
62ca3888d3 历史统计数据,错误提示 2025-12-18 15:20:54 +08:00
3ec56a925c 修改搜索条件 2025-12-18 10:33:24 +08:00
95c08818cb 取消dark主题 2025-12-18 09:03:08 +08:00
42355bd1ef 站点-新增历史记录 2025-12-17 17:40:02 +08:00
fe2ce75cec 修改绑定车辆接口
登录后查询车辆信息
2025-12-17 09:18:34 +08:00
98cac8a0a5 预约列表增加筛选条件 2025-12-16 16:18:28 +08:00
9ce46a0c7d 加氢站默认选择,演示账号数据
附件默认显示,详情
2025-12-16 11:58:00 +08:00
266a43c09d 主题色 2025-12-16 09:54:32 +08:00
8434301d1f 版本号 2025-12-12 10:34:24 +08:00
adfe3bf34e 版本号修改 2025-12-11 15:50:25 +08:00
6a3c6db7a8 Merge branch 'dev'
司机端-加氢预约  预约时间段修改  1、开始结束时间整合成一个  2、时间只开放当日和次日
司机端-加氢预约  提交预约不可提交限制 1、非营业站点 2、预约时间不可重复
司机端-预约列表 1、新增到站时间、氢量修改 2、显示拒绝加氢原因
加氢站-加氢预约  1、新增当日预约加车牌/手机号筛选  2、新增加氢总量 未加氢量  3、 确认拒绝流程优化:完成可填写具体加氢量,新增拒绝原因
# Conflicts:
#	ln_jq_app/lib/common/styles/theme.dart
2025-12-11 15:48:10 +08:00
4ace3c9f27 刷新条件 2025-12-11 13:45:26 +08:00
b52659df6c 司机预约 显示拒绝原因 2025-12-11 13:27:28 +08:00
20877a3eb1 增加弹窗限制条件 2025-12-11 10:36:59 +08:00
69875f27ad 搜索字段 2025-12-10 17:51:00 +08:00
829f5e454e 文本处理 2025-12-10 17:37:56 +08:00
1c916a7fad 调整司机预约时间
站点工单状态
2025-12-10 17:34:46 +08:00
88b24b5228 修改配置ios 2025-12-10 13:16:10 +08:00
ee8cbde296 logo 2025-12-09 13:16:09 +08:00
f638704fba ios 说明 2025-12-09 11:24:45 +08:00
4c63d5ebf3 bugfix 2025-12-08 17:52:55 +08:00
a3fb0d8018 站点 确认拒绝流程 2025-12-08 09:32:06 +08:00
88b16ca69e 今日预约搜索 2025-12-03 16:00:18 +08:00
ba3467810c 新增数据 2025-12-03 15:04:58 +08:00
0bfedd54cb 可编辑预约 2025-12-03 10:39:34 +08:00
f25d7e4567 增加 加氢站可用标记 2025-12-02 10:36:56 +08:00
4cdedff654 默认时间段修改 2025-12-01 17:03:16 +08:00
4f99ab4164 修改选择器 2025-12-01 15:27:00 +08:00
cdc5af7f45 限制 2025-12-01 13:59:02 +08:00
20e5e58ded 增加提交预约限制 2025-12-01 13:56:17 +08:00
93a39e440a v1.1.0 2025-11-28 14:48:03 +08:00
ec1c554eb3 Merge branch 'dev'
v1.1.0
2025-11-28 10:12:44 +08:00
d3b7ab8fbe 先限制输入起点 2025-11-28 10:09:18 +08:00
5609594439 地图修改 2025-11-27 17:21:07 +08:00
da4149ec60 调整地图样式 2025-11-27 15:25:58 +08:00
aa30d13b91 更新插件 2025-11-27 09:30:52 +08:00
b854d295f9 调整预约时间格式 2025-11-26 17:33:04 +08:00
26a24efeb8 限制条件 2025-11-26 13:44:00 +08:00
496d77482d logo 预约弹出列表
车辆无绑定弹窗
2025-11-26 09:20:42 +08:00
130 changed files with 10239 additions and 3180 deletions

View File

@@ -1,3 +1,6 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("kotlin-android")
@@ -5,6 +8,14 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { input ->
keystoreProperties.load(input.reader(Charsets.UTF_8))
}
}
android {
namespace = "com.lnkj.ln_jq_app"
compileSdk = flutter.compileSdkVersion
@@ -21,20 +32,35 @@ android {
defaultConfig {
// 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.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
versionCode = 6
versionName = "1.2.3"
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
val storeFilePath = keystoreProperties["storeFile"] as String?
storeFile = if (storeFilePath != null) file(storeFilePath) else null
storePassword = keystoreProperties["storePassword"] as String?
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
getByName("release") {
// 使用上面定义的 release 签名
signingConfig = signingConfigs.getByName("release")
// 修复混淆规则引用语法
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}

View File

@@ -0,0 +1,61 @@
# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin.
# 忽略 Google Play Core 相关的缺失警告(解决你目前的报错)
-dontwarn com.google.android.play.core.**
# Flutter 基础规则
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-dontwarn com.huawei.android.os.BuildEx$VERSION
-dontwarn com.huawei.hianalytics.process.HiAnalyticsConfig$Builder
-dontwarn com.huawei.hianalytics.process.HiAnalyticsConfig
-dontwarn com.huawei.hianalytics.process.HiAnalyticsInstance$Builder
-dontwarn com.huawei.hianalytics.process.HiAnalyticsInstance
-dontwarn com.huawei.hianalytics.process.HiAnalyticsManager
-dontwarn com.huawei.hianalytics.util.HiAnalyticTools
-dontwarn com.huawei.hms.availableupdate.UpdateAdapterMgr
-dontwarn com.huawei.libcore.io.ExternalStorageFile
-dontwarn com.huawei.libcore.io.ExternalStorageFileInputStream
-dontwarn com.huawei.libcore.io.ExternalStorageFileOutputStream
-dontwarn com.huawei.libcore.io.ExternalStorageRandomAccessFile
-dontwarn org.android.netutil.PingEntry
-dontwarn org.android.netutil.PingResponse
-dontwarn org.android.netutil.PingTask
-dontwarn org.bouncycastle.crypto.BlockCipher
-dontwarn org.bouncycastle.crypto.engines.AESEngine
-dontwarn org.bouncycastle.crypto.prng.SP800SecureRandom
-dontwarn org.bouncycastle.crypto.prng.SP800SecureRandomBuilder
-keepclasseswithmembernames class ** {
native <methods>;
}
-keepattributes Signature
-keep class sun.misc.Unsafe { *; }
-keep class com.taobao.** {*;}
-keep class com.alibaba.** {*;}
-keep class com.alipay.** {*;}
-keep class com.ut.** {*;}
-keep class com.ta.** {*;}
-keep class anet.**{*;}
-keep class anetwork.**{*;}
-keep class org.android.spdy.**{*;}
-keep class org.android.agoo.**{*;}
-keep class android.os.**{*;}
-keep class org.json.**{*;}
-dontwarn com.taobao.**
-dontwarn com.alibaba.**
-dontwarn com.alipay.**
-dontwarn anet.**
-dontwarn org.android.spdy.**
-dontwarn org.android.agoo.**
-dontwarn anetwork.**
-dontwarn com.ut.**
-dontwarn com.ta.**

View File

@@ -15,11 +15,13 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_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.POST_NOTIFICATIONS"/>
<application
android:label="小羚羚"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/logo">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -48,6 +50,64 @@
android:name="flutterEmbedding"
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安全建议设置不可导出如需对其他应用开放可通过androidpermission进行限制 -->
<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>
<!-- Required to query activities that can process text, see:

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -3,6 +3,17 @@ allprojects {
// 使用阿里云镜像
maven("https://maven.aliyun.com/repository/public")
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()
mavenCentral()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,224 +1,644 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<title>导航规划</title>
<style>
html, body, #container { width: 100%; height: 100%; margin: 0; }
<title>路径规划</title>
/* 搜索栏样式 */
<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;
}
/* 去除高德默认的 label 边框 and 背景 */
.amap-marker-label {
border: none !important;
background-color: transparent !important;
}
/* 自定义气泡样式 */
.custom-bubble {
position: relative;
background: rgba(51, 51, 51, 0.7);
/* #33333399 对应 rgba(51,51,51,0.7) */
color: #fff;
padding: 6px 15px;
border-radius: 20px;
/* 圆角 */
font-size: 13px;
white-space: nowrap;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
text-align: center;
line-height: 1.4;
}
/* 气泡下方的向下小箭头 */
.custom-bubble::after {
content: '';
position: absolute;
bottom: -6px;
/* 箭头高度 */
left: 50%;
transform: translateX(-50%);
border-width: 6px 6px 0 6px;
border-style: solid;
border-color: rgba(51, 51, 51, 0.7) transparent transparent transparent;
}
#panel .amap-call {
display: none;
}
/* --- 搜索栏样式 --- */
#search-box {
position: absolute;
top: 10px;
top: 40px;
left: 10px;
right: 10px;
z-index: 100;
background: #fff;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
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: #017143FF;
color: #fff;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
}
/* --- 导航结果面板 (底部弹出) --- */
#panel {
position: fixed;
bottom: 0;
bottom: 95px;
left: 0;
width: 100%;
height: 30%; /* 占据底部30% */
height: 35%;
/* 面板高度 */
background-color: white;
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;
display: none; /* 默认隐藏,规划成功后显示 */
display: none;
/* 默认隐藏 */
}
/* --- 自定义定位按钮样式 --- */
#location-btn {
position: fixed;
right: 10px;
bottom: 105px;
/* 默认位置 */
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;
}
/* --- 调整比例尺位置 --- */
.amap-scalecontrol {
/* 初始状态:避开底部的定位按钮或留出安全间距 */
bottom: 110px !important;
left: 10px !important;
transition: bottom 0.3s ease;
/* 增加平滑动画 */
}
/* --- 当路径规划面板显示时,比例尺自动上移 --- */
body.panel-active .amap-scalecontrol {
bottom: 38% !important;
/* 移动到面板上方 (面板高度35% + 3%间距) */
}
/* --- 关键:当 body 有 panel-active 类时,按钮上移 --- */
body.panel-active #location-btn {
bottom: 45%;
/* 对应 #panel 的 height + 一点间距 */
}
</style>
<!-- 1. 必须最先配置安全密钥 -->
<!-- 1. 配置安全密钥 -->
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: '0529b72df6bf0c577ff2182cb8b1d970',
securityJsCode: 'aa3a22c19ed76b27f8a587555d6981c8',
}
</script>
<!-- 2. 加载地图和插件 -->
<!-- 注意如果你要货车规划需要在plugin里加上 AMap.TruckDriving -->
<script src="https://webapi.amap.com/maps?v=2.0&key=2cc1d822e313307fe311c3127a1deeb5&plugin=AMap.MoveAnimation,AMap.Driving,AMap.TruckDriving,AMap.AutoComplete"></script>
<!-- 2. 加载地图和插件 (去掉了 Geolocation 插件,避免弹窗) -->
<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>
<body>
<div id="search-box">
<div class="input-row">
<input id="startInput" placeholder="起点: 默认使用当前位置" />
<div id="search-box">
<div class="input-row">
<input id="startInput" placeholder="起点: 请输入当前地点" />
</div>
<div class="input-row">
<input id="endInput" placeholder="终点: 请输入目的地" />
<button onclick="startRouteSearch()">路径规划</button>
</div>
</div>
<div class="input-row">
<input id="endInput" placeholder="终点: 请输入目的地" />
<button onclick="startRouteSearch()">路径规划</button>
<div id="container"></div>
<div id="panel"></div>
<!-- 自定义定位按钮 -->
<div id="location-btn" onclick="backToLocation()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path
d="M512 85.333333a426.666667 426.666667 0 1 0 0 853.333334 426.666667 426.666667 0 0 0 0-853.333334z m0 149.333334a277.333333 277.333333 0 1 1 0 554.666666 277.333333 277.333333 0 0 1 0-554.666666z m0 149.333333a128 128 0 1 0 0 256 128 128 0 0 0 0-256z"
fill="#666666"></path>
</svg>
</div>
</div>
<div id="container"></div>
<div id="panel"></div>
<script>
var map, marker, destMarker, driving, truckDriving, geocoder;
var currentLat, currentLng;
var isTruckMode = false;
var isInitialLocationSet = false;
var stationMarkers = []; // 存储所有站点的标记
<script>
var map, marker, driving, truckDriving;
var currentLat, currentLng;
// 标记是否使用货车模式 (true: 货车, false: 轿车)
var isTruckMode = false;
function initMap() {
map = new AMap.Map('container', {
resizeEnable: true,
zoom: 17,
viewMode: '3D'
});
function initMap() {
map = new AMap.Map('container', {
resizeEnable: true,
zoom: 15,
viewMode: '3D'
});
// --- 2. 初始化 geocoder ---
geocoder = new AMap.Geocoder({
city: "全国" // 设置地理编码范围
});
// --- 初始化轿车规划 ---
driving = new AMap.Driving({
map: map,
panel: "panel", // 结果显示在 panel div 中
policy: AMap.DrivingPolicy.LEAST_TIME
});
// 通知 Flutter 地图加载完毕
map.on('complete', function () {
console.log("JS->: Map is ready.");
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler('mapReady');
}
});
// --- 初始化货车规划 (按需开启) ---
// 货车需要设置 size (1:微型, 2:轻型, 3:中型, 4:重型)
if(AMap.TruckDriving) {
truckDriving = new AMap.TruckDriving({
// 添加基础控件
map.addControl(new AMap.Scale());
map.addControl(new AMap.ToolBar({
visible: true,
position: {
top: '200px',
right: '10px'
} // 稍微避开定位按钮
}));
// --- 初始化轿车规划 ---
driving = new AMap.Driving({
map: map,
panel: "panel",
size: 2,
policy: 0, // 0: 躲避拥堵
width: 2.5,
height: 2,
load: 1,
weight: 10,
axlesNum: 2,
policy: AMap.DrivingPolicy.LEAST_TIME
});
// --- 初始化货车规划 ---
if (AMap.TruckDriving) {
truckDriving = new AMap.TruckDriving({
map: map,
panel: "panel",
size: 2,
policy: 0,
width: 2.5,
height: 2,
load: 1,
weight: 10,
axlesNum: 2,
});
}
// --- 输入提示 ---
new AMap.AutoComplete({
input: "startInput"
});
new AMap.AutoComplete({
input: "endInput"
});
}
// --- 输入提示 (自动补全) ---
new AMap.AutoComplete({ input: "startInput" });
new AMap.AutoComplete({ input: "endInput" });
}
/**
* 核心功能 1: 接收 Flutter 传来的定位数据
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
* 纬度 经度
*/
function updateMyLocation(lat, lng, angle) {
var rawLat = parseFloat(lat);
var rawLng = parseFloat(lng);
var rawAngle = parseFloat(angle);
var gps = [rawLng, rawLat];
// --- 被动接收 Flutter 传来的定位 ---
function updateMyLocation(lat, lng, angle) {
currentLat = lat;
currentLng = lng;
var position = [lng, lat];
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) {
marker = new AMap.Marker({
// 更新车辆标记位置 (保持不变)
if (!marker) {
marker = new AMap.Marker({
map: map,
position: position,
icon: "car.png",
offset: new AMap.Pixel(-23.5, -15),
autoRotation: true,
angle: isNaN(rawAngle) ? 0 : rawAngle,
});
map.setCenter(position);
map.setZoom(13);
} else {
marker.moveTo(position, {
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;
console.log("地理:" + JSON.stringify(result));
fetchStationInfo(addressComponent.province, addressComponent.city,
addressComponent.district, lat, lng);
fetchStationInfoList(lat, lng);
// 策略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.district +
addressComponent.township +
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);
}
});
}
}
});
}
/**
* 调用后端接口获取站点
*/
function fetchStationInfo(province, city, district, lat, lng) {
// 注意:某些直辖市在高德中 city 字段可能为空,需做兼容处理
console.log("JS->: 开始请求." + province + city + district);
var cityName = (typeof city === 'string' && city.length > 0) ? city : province;
fetch('https://beta-esg.api.lnh2e.com/appointment/station/getStationInfoByArea', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
province: province,
city: cityName,
district: district,
longitude: lng,
latitude: lat,
})
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误: ' + response.status);
}
return response.json(); // 解析 JSON
})
.then(res => {
// 打印完整的返回结果,方便调试观察结构
console.log("JS->: 接口完整返回:", JSON.stringify(res));
// 安全校验:判断 res.data 是否存在
if (res.code === 0 && res.data) {
if (res.data.address) {
console.log("JS->: 找到地址:", res.data.address);
var destAddress = res.data.address;
document.getElementById('endInput').value = destAddress;
// 标记终点
markDestination(destAddress, res.data.name || "目的地",
res.data.longitude, res.data.latitude
);
} else {
console.log("JS->: 接口请求成功,但该区域暂无站点地址");
}
} else {
console.log("JS->: 业务报错或无数据:", res.message);
}
})
.catch(err => console.error('JS->:获取站点信息失败:', err));
}
/**
* 获取站点列表
*/
function fetchStationInfoList(lat, lng) {
fetch('https://beta-esg.api.lnh2e.com/appointment/station/getNearbyHydrogenStationsByLocation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
longitude: lng,
latitude: lat,
})
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误: ' + response.status);
}
return response.json(); // 解析 JSON
})
.then(res => {
console.log("JS->:2 接口完整返回:", JSON.stringify(res));
if (res.code === 0 && res.data && Array.isArray(res.data)) {
// 1. 清除旧的站点标记
stationMarkers.forEach(m => m.setMap(null));
stationMarkers = [];
// 2. 循环标记所有加氢站
res.data.forEach(station => {
var stationIcon = new AMap.Icon({
size: new AMap.Size(32, 32),
image: 'ic_tag.png',
imageSize: new AMap.Size(32, 32)
});
var sMarker = new AMap.Marker({
map: map,
position: [station.longitude, station.latitude],
icon: stationIcon,
offset: new AMap.Pixel(-16, -32),
title: station.name,
label: {
content: '<div class="custom-bubble">' + station.name + '</div>',
direction: 'top'
}
});
// 3. 绑定点击事件:选中即为目的地,并开始规划
sMarker.on('click', function() {
document.getElementById('endInput').value = station.address || station.name;
// 更新当前的 destMarker (如果需要)
if (destMarker) destMarker.setMap(null);
destMarker = sMarker;
startRouteSearch();
});
stationMarkers.push(sMarker);
});
} else {
console.log("JS->: 业务报错或无数据:", res.message);
}
})
.catch(err => console.error('JS->:获取站点信息失败:', err));
}
/**
* 地理编码并在地图标记终点
*/
function markDestination(address, name, longitude, latitude) {
// 1. 清除旧的终点标记
if (destMarker) destMarker.setMap(null);
// 2. 创建自定义图标
var destIcon = new AMap.Icon({
size: new AMap.Size(32, 32), // 图标尺寸
image: 'ic_tag.png', // 本地图片路径
imageSize: new AMap.Size(32, 32) // 图片在图标内拉伸的大小
});
// 3. 创建标记
destMarker = new AMap.Marker({
map: map,
position: position,
icon: "https://webapi.amap.com/images/car.png",
offset: new AMap.Pixel(-26, -13),
autoRotation: true,
angle: angle || 0,
position: [longitude, latitude],
icon: destIcon, // 使用自定义图标
offset: new AMap.Pixel(-16, -32),
title: name,
label: {
content: '<div class="custom-bubble">' + name + '</div>',
direction: 'top'
}
});
// 第一次定位移动中心
map.setCenter(position);
} else {
marker.setPosition(position);
if (angle) marker.setAngle(angle);
}
}
// --- 核心:规划路线 ---
function startRouteSearch() {
var startKw = document.getElementById('startInput').value;
var endKw = document.getElementById('endInput').value;
if(!endKw) {
console.log("FlutterLog: 终点不能为空");
return;
console.log("JS->: 终点标记已添加", address);
}
console.log("FlutterLog: 开始规划..." + (isTruckMode ? "货车" : "轿车"));
// 清除之前的路线
if(driving) driving.clear();
if(truckDriving) truckDriving.clear();
// 构造起点和终点参数
// 高德API Search方法支持点对象数组[{keyword: '名字', city: '城市'}, {keyword: '...'}]
var points = [];
// 1. 处理起点
if (startKw) {
// 如果用户输入了文字
points.push({ keyword: startKw });
} else {
// 如果用户没输入,使用当前定位
/**
* 点击按钮回到当前位置
*/
function backToLocation() {
if (currentLng && currentLat) {
// 关键点:当混合使用坐标和关键字时,必须构建 LngLat 对象
map.panTo([currentLng, currentLat]);
map.setZoom(17);
console.log("JS: 已回到当前位置");
} else {
console.log("JS: 暂无定位数据,向 Flutter 请求...");
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler('requestLocation');
}
}
}
/**
* 路径规划
*/
function startRouteSearch() {
// 获取输入框的文字
var startKw = document.getElementById('startInput').value;
var endKw = document.getElementById('endInput').value;
if (!startKw) {
alert("请输入起点");
return;
}
if (!endKw) {
alert("请输入终点");
return;
}
// 清除旧路线
if (driving) driving.clear();
// 收起键盘
document.getElementById('startInput').blur();
document.getElementById('endInput').blur();
// --- 构造路径规划的点 ---
var points = [];
// 1. 处理起点逻辑
if (!startKw || startKw === '我的位置' || startKw.includes('当前位置')) {
if (!currentLng || !currentLat) {
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler('requestLocation');
}
alert("正在获取定位,请稍后...");
return;
}
points.push({
keyword: '我的位置',
location: new AMap.LngLat(currentLng, currentLat)
});
} else {
console.log("FlutterLog: 无定位数据且无输入");
alert("未获取到定位,请输入起点");
return;
points.push({
keyword: startKw
});
}
// 2. 处理终点逻辑
points.push({
keyword: endKw
});
// 3. 发起搜索
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("规划失败,请检查起终点名称");
}
});
}
// 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需要更严格的经纬度参数建议先使用轿车演示");
// 辅助功能:手动绘制路线 (Flutter 可能用到)
function drawCustomRoute(pointsJsonString) {
var path = typeof pointsJsonString === 'string' ? JSON.parse(pointsJsonString) : pointsJsonString;
if (driving) driving.clear();
var polyline = new AMap.Polyline({
path: path,
isOutline: true,
outlineColor: '#ffeeee',
borderWeight: 3,
strokeColor: "#3366FF",
strokeWeight: 6,
lineJoin: 'round'
});
map.add(polyline);
map.setFitView([polyline]);
}
// 默认使用轿车规划 (支持 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();
var polyline = new AMap.Polyline({
path: path,
isOutline: true,
outlineColor: '#ffeeee',
borderWeight: 3,
strokeColor: "#3366FF",
strokeWeight: 6,
lineJoin: 'round'
});
map.add(polyline);
map.setFitView([polyline]);
}
window.onload = initMap;
</script>
window.onload = initMap;
</script>
</body>
</html>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

View File

@@ -1,6 +1,9 @@
# 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.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -1,9 +1,19 @@
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):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
@@ -20,7 +30,9 @@ PODS:
- FlutterMacOS
- image_picker_ios (0.0.1):
- Flutter
- MTBBarcodeScanner (5.0.11)
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
@@ -29,9 +41,6 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- qr_code_scanner_plus (0.2.6):
- Flutter
- MTBBarcodeScanner
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -39,33 +48,43 @@ PODS:
- Flutter
DEPENDENCIES:
- aliyun_push_flutter (from `.symlinks/plugins/aliyun_push_flutter/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- 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`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- 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`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
https://gitee.com/aliyun/aliyun-specs.git:
- AlicloudUTDID
https://github.com/aliyun/aliyun-specs.git:
- AlicloudELS
- AlicloudPush
trunk:
- MTBBarcodeScanner
- OrderedSet
EXTERNAL SOURCES:
aliyun_push_flutter:
:path: ".symlinks/plugins/aliyun_push_flutter/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_native_splash:
@@ -76,37 +95,41 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/geolocator_apple/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
qr_code_scanner_plus:
:path: ".symlinks/plugins/qr_code_scanner_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
AlicloudELS: fbf821383330465a5af84a033f36f263ae46ca41
AlicloudPush: 95150880af380f64cf1741f5586047c17d36c1d9
AlicloudUTDID: 5d2f22d50e11eecd38f30bc7a48c71925ea90976
aliyun_push_flutter: 0fc2f048a08687ef256c0cfdd72dd7a550ef3347
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_pdfview: 54e283d5851b0b247b3cc57877d35f1a05a204de
flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
qr_code_scanner_plus: 7e087021bc69873140e0754750eb87d867bed755
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: 6416011b1bc721211379eaad259ff1cba3dbfad2
PODFILE CHECKSUM: 357c01ff4e7591871e8c4fd6462220a8c7447220
COCOAPODS: 1.16.2

View File

@@ -50,6 +50,7 @@
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>"; };
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>"; };
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>"; };
@@ -143,6 +144,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
6D3F89E22F04C32900A154AD /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -487,7 +489,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
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;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -495,11 +500,89 @@
"$(inherited)",
"@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_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_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
@@ -674,7 +757,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
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;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -682,12 +768,90 @@
"$(inherited)",
"@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_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_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -698,7 +862,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
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;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -706,11 +873,56 @@
"$(inherited)",
"@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_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_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;

View File

@@ -1,122 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
"scale" : "3x",
"size" : "29x29"
},
{
"size" : "40x40",
"filename" : "Icon-App-80x80.jpg",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
"scale" : "2x",
"size" : "40x40"
},
{
"size" : "60x60",
"filename" : "Icon-App-120x120.jpg",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
"scale" : "3x",
"size" : "40x40"
},
{
"size" : "60x60",
"filename" : "Icon-App-120x120 1.jpg",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "Icon-App-180.jpg",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"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",
"scale" : "1x"
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -2,16 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要您的位置信息以在地图上展示</string>
<!-- 建议添加即使你只申请WhenInUse有些插件逻辑可能会检查这个key -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>我们需要您的位置来规划路线</string>
<!-- 建议添加旧版本iOS兼容 -->
<key>NSLocationAlwaysUsageDescription</key>
<string>我们需要您的位置来规划路线</string>
<key></key>
<string></string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -32,8 +26,24 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<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>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -51,17 +61,20 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>uses</key>
<string></string>
<key>NSCameraUsageDescription</key>
<string>需要访问您的相机以扫描二维码</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问您的相册以选择二维码图片进行识别</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要访问您的相册以选择二维码图片进行识别</string>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
</array>
<key>CFBundleLocalizations</key>
<array>
<string>zh-Hans</string>
<string>en</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View 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>

View File

@@ -0,0 +1,22 @@
import 'package:getx_scaffold/common/index.dart';
import 'package:ln_jq_app/pages/login/view.dart';
import 'package:ln_jq_app/storage_service.dart';
class AuthGuard {
static bool _handling401 = false;
static Future<void> handle401(String? message) async {
if (_handling401) return;
_handling401 = true;
try {
await StorageService.to.clearLoginInfo();
Get.offAll(() => const LoginPage());
} finally {
// 防止意外卡死,可视情况是否延迟重置
Future.delayed(const Duration(seconds: 1), () {
_handling401 = false;
});
}
}
}

View File

@@ -1,4 +1,5 @@
import 'package:encrypt/encrypt.dart';
import 'package:flutter/material.dart' as ui;
class LoginUtil {
static final _keyString = '915eae87951a448c86c47796e44c1fcf';
@@ -26,5 +27,9 @@ class LoginUtil {
final decrypted = _encrypter.decrypt(encrypted);
return decrypted;
}
static ui.Image getAssImg(String imgName){
return ui.Image(image: ui.AssetImage('assets/images/$imgName.png'),fit: ui.BoxFit.cover,);
}
}

View File

@@ -6,7 +6,6 @@ class BaseModel<T> {
final String message; // 消息,例如 "success"
final String msg; // 消息,例如 "success"
final T? data; // 核心数据,使用泛型 T可以是任何类型
final int time; // 时间戳
final dynamic error; // 错误信息,可以是任何类型或 null
BaseModel({
@@ -15,7 +14,6 @@ class BaseModel<T> {
required this.message,
required this.msg,
this.data, // data 可以为 null
required this.time,
this.error, // error 可以为 null
});
@@ -60,7 +58,6 @@ class BaseModel<T> {
status: json['status'] as bool? ?? false,
message: json['message'] ?? '暂不可用,请稍后',
msg: json['msg'] ?? '暂不可用,请稍后',
time: _parseInt(json['time']),
data: finalData,
error: json['error'],
);
@@ -72,7 +69,6 @@ class BaseModel<T> {
'status': status,
'message': message,
'msg': msg,
'time': time,
'data': data,
'error': error,
};

View File

@@ -4,6 +4,9 @@ class StationModel {
final String address;
final String price;
final String siteStatusName; // 例如 "维修中"
final int isSelect; // 1是可用 0是不可用
final String startBusiness; // 新增:可预约最早开始时间,如 "06:00:00"
final String endBusiness; // 新增:可预约最晚结束时间,如 "22:00:00"
StationModel({
required this.hydrogenId,
@@ -11,9 +14,11 @@ class StationModel {
required this.address,
required this.price,
required this.siteStatusName,
required this.isSelect,
required this.startBusiness,
required this.endBusiness,
});
// 从 JSON map 创建对象的工厂构造函数
factory StationModel.fromJson(Map<String, dynamic> json) {
return StationModel(
hydrogenId: json['hydrogenId'] ?? '',
@@ -21,6 +26,9 @@ class StationModel {
address: json['address'] ?? '地址未知',
price: json['price']?.toString() ?? '0.00',
siteStatusName: json['siteStatusName'] ?? '',
isSelect: json['isSelect'] as int? ?? 0,
startBusiness: json['startBusiness'] ?? '00:00:00', // 默认全天
endBusiness: json['endBusiness'] ?? '23:59:59', // 默认全天
);
}
}

View File

@@ -15,7 +15,7 @@ class VehicleInfo {
final String engineNum;
final String truckNum;
final num hydrogenCapacity;
final num maxHydrogen;
final String maxHydrogen;
VehicleInfo({
required this.plateNumber,
@@ -36,7 +36,7 @@ class VehicleInfo {
engineNum: json["engineNum"] ?? '',
truckNum: json["truckNum"] ?? '',
hydrogenCapacity: json["hydrogenCapacity"] ?? 0,
maxHydrogen: json["maxHydrogen"] ?? 0,
maxHydrogen: json["maxHydrogen"] ?? '',
);
Map<String, dynamic> toJson() => {

View File

@@ -5,13 +5,20 @@ class AppTheme {
static const String Font_YuYang = 'YuYang';
static const Color themeColor = Color(0xFF0c83c3);
static const Color themeColor = Color(0xFF017137);
//是否开放域名切换
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 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/";
@@ -19,6 +26,11 @@ class AppTheme {
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(
useMaterial3: false,
@@ -56,12 +68,9 @@ class AppTheme {
appBarTheme: const AppBarTheme(
backgroundColor: Color.fromARGB(255, 34, 34, 34),
centerTitle: true,
titleTextStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
bottomAppBarTheme: BottomAppBarThemeData(
bottomAppBarTheme: BottomAppBarThemeData(
color: Color.fromARGB(255, 34, 34, 34),
elevation: 4.0,
),

View File

@@ -1,16 +1,20 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/storage_service.dart';
/// 专门用于处理和添加 Token 的拦截器
class TokenInterceptor extends Interceptor {
// 定义您想要使用的 Token Key
final String tokenKey;
final String sourceKey;
// 构造函数,允许外部传入自定义的 Key默认为 'Authorization'
TokenInterceptor({this.tokenKey = 'Authorization'});
TokenInterceptor({this.tokenKey = 'Authorization', this.sourceKey = 'source'});
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async{
// 从 StorageService 中获取已保存的 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) 以继续执行请求
// 这一步至关重要,否则请求会被中断
super.onRequest(options, handler);

View File

@@ -1,14 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get_storage/get_storage.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/AuthGuard.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/storage_service.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'common/styles/theme.dart';
import 'pages/home/view.dart';
import 'pages/login/view.dart';
import 'pages/welcome/view.dart'; // 引入启动页
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -16,8 +19,10 @@ void main() async {
WidgetsBinding widgetsBinding = await init(
isDebug: true,
logTag: '小羚羚',
supportedLocales: [Locale('zh', 'CN')],
supportedLocales: [const Locale('zh', 'CN')],
);
// 保持原生闪屏页,直到 WelcomeController 调用 remove()
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await GetStorage.init();
@@ -30,7 +35,7 @@ void main() async {
// 设计稿尺寸 单位dp
designSize: const Size(390, 844),
// Getx Log
enableLog: false,
enableLog: true,
// 默认的跳转动画
defaultTransition: Transition.rightToLeft,
// 主题模式
@@ -38,23 +43,20 @@ void main() async {
// 主题
theme: AppTheme.light,
// Dark主题
darkTheme: AppTheme.dark,
darkTheme: AppTheme.light,
// AppTitle
title: '小羚羚',
// 首页入口
home: HomePage(),
//组件国际化
fallbackLocale: Locale('zh', 'CN'),
supportedLocales: [Locale('zh', 'CN')],
// 将入口改为启动页
home: const WelcomePage(),
fallbackLocale: const Locale('zh', 'CN'),
supportedLocales: const [Locale('zh', 'CN')],
localizationsDelegates: const [
RefreshLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// Builder
builder: (context, widget) {
// do something....
return widget!;
},
),
@@ -62,22 +64,23 @@ void main() async {
}
void initHttpSet() {
// 设置基础 URL
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
HttpService.to.init(timeout: 15);
HttpService.to.setBaseUrl(AppTheme.test_service_url);
//指定请求头
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
// 设置全局响应处理器
HttpService.to.setOnResponseHandler((response) async {
try {
final baseModel = BaseModel<dynamic>.fromJson(response.data);
final baseModel = BaseModel.fromJson(response.data);
if (baseModel.code == 0 || baseModel.code == 200) {
return null;
} else if (baseModel.code == 401) {
await StorageService.to.clearLoginInfo();
Get.offAll(() => LoginPage());
await AuthGuard.handle401(baseModel.message);
return baseModel.message;
} else {
return baseModel.message;
return (baseModel.error.toString()).isEmpty
? "服务繁忙,稍后重试"
: baseModel.error.toString();
}
} on Exception catch (e) {
e.printInfo();

View File

@@ -1,22 +1,25 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/common/index.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/b_page/base_widgets/controller.dart';
import 'package:ln_jq_app/pages/b_page/reservation/view.dart';
import 'package:ln_jq_app/pages/b_page/site/view.dart';
class B_BaseWidgetsPage extends GetView<B_BaseWidgetsController> {
B_BaseWidgetsPage({super.key});
B_BaseWidgetsPage({super.key});
final PageController _pageController = PageController();
// 主视图
Widget _buildView() {
return PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(), // 禁止滑动
onPageChanged: (index) {
jumpTabAndPage(index);
},
children: _buildPages(), // 页面的列表
children: _buildPages(),
);
}
@@ -25,35 +28,59 @@ class B_BaseWidgetsPage extends GetView<B_BaseWidgetsController> {
controller.updateUi(); // 更新 UI
_pageController.jumpToPage(controller.pageIndex);
}
// 对应的页面
List<Widget> _buildPages() {
return [
SitePage(),
ReservationPage(),
];
return [SitePage(), ReservationPage()];
}
//导航栏
// 自定义导航栏 (悬浮胶囊样式)
Widget _buildNavigationBar() {
return NavigationX(
currentIndex: controller.pageIndex, // 当前选中的tab索引
onTap: (index) {
jumpTabAndPage(index);
}, // 切换tab事件
items: [
NavigationItemModel(
label: '加氢预约',
icon: AntdIcon.orderedlist,
selectedIcon: AntdIcon.calendar_fill,
badge: '99+',
dot: true,
return SafeArea(
child: Container(
height: 50.h,
margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
decoration: BoxDecoration(
color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
NavigationItemModel(
label: '站点信息',
icon: AntdIcon.car,
selectedIcon: AntdIcon.car_fill,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildNavItem(0, "ic_h2_select@2x", "ic_h2@2x"),
_buildNavItem(1, "ic_h2_my@2x", "ic_h2_my_select@2x"),
],
),
],
),
);
}
// 构建单个导航项
Widget _buildNavItem(int index, String icon, String selectedIcon) {
bool isSelected = controller.pageIndex == index;
return GestureDetector(
onTap: () => jumpTabAndPage(index),
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(horizontal: 50.w, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF006633) : Colors.transparent, // 选中时的深绿色背景
borderRadius: BorderRadius.circular(20),
),
child: SizedBox(
height: 24,
width: 24,
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),
),
),
);
}
@@ -64,10 +91,10 @@ class B_BaseWidgetsPage extends GetView<B_BaseWidgetsController> {
id: 'b_baseWidgets',
builder: (_) {
return Scaffold(
extendBody: false,
extendBody: true,
resizeToAvoidBottomInset: false,
bottomNavigationBar: _buildNavigationBar(),
body: SafeArea(child: _buildView()),
body: _buildView(),
);
},
);

View File

@@ -0,0 +1,150 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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'; // Reuse ReservationModel
class HistoryController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'history';
// --- 定义 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'.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 = "";
final Map<String, String> statusOptions = {
'': '全部',
'0': '待加氢',
'1': '已加氢',
'2': '未加氢',
'5': '拒绝加氢',
};
final RxString selectedStatus = ''.obs;
final RxString selectedDateType = ''.obs; // week, month, three_month
@override
void onInit() {
super.onInit();
final args = Get.arguments as Map<String, dynamic>;
stationName = args['stationName'] as String? ?? "";
refreshData();
}
void refreshData() {
getAllOrderCounts();
fetchHistoryData();
}
Future<void> getAllOrderCounts() async {
var response = await HttpService.to.post(
"appointment/orderAddHyd/getAllOrderCounts",
data: {
/*'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),*/
'plateNumber': plateNumberController.text,
'stationName': stationName,
"status": selectedStatus.value,
"dateType": selectedDateType.value,
},
);
if (response == null || response.data == null) {
totalHydrogen.value = '0';
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}';
totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0}';
} catch (e) {
totalHydrogen.value = '0';
totalCompletions.value = '0';
}
}
Future<void> fetchHistoryData() async {
isLoading.value = true;
updateUi();
try {
var response = await HttpService.to.post(
"appointment/orderAddHyd/sitOrderPage",
data: {
/*'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),*/
'plateNumber': plateNumberController.text,
'pageNum': 1,
'pageSize': 50,
'stationName': stationName,
"status": selectedStatus.value,
"dateType": selectedDateType.value,
},
);
if (response == null || response.data == null) {
_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 {
_resetData();
}
} catch (e) {
_resetData();
} finally {
isLoading.value = false;
updateUi();
}
}
void _resetData() {
historyList.clear();
hasData.value = false;
}
void onStatusSelected(String status) {
if (selectedStatus.value == status) return;
selectedStatus.value = status;
refreshData();
}
void onDateTypeSelected(String type) {
selectedDateType.value = type;
refreshData();
}
@override
void onClose() {
plateNumberController.dispose();
super.onClose();
}
}

View File

@@ -0,0 +1,314 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/b_page/history/controller.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart';
class HistoryPage extends GetView<HistoryController> {
const HistoryPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<HistoryController>(
init: HistoryController(),
id: 'history',
builder: (_) {
return Scaffold(
backgroundColor: const Color(0xFFF7F8FA),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20),
onPressed: () => Get.back(),
),
title: _buildSearchBox(),
),
body: Column(
children: [
_buildFilterBar(),
_buildSummaryCard(),
Expanded(child: _buildHistoryList()),
],
),
);
},
);
}
Widget _buildSearchBox() {
return Container(
height: 36,
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(18),
),
child: TextField(
controller: controller.plateNumberController,
onSubmitted: (v) => controller.refreshData(),
decoration: const InputDecoration(
hintText: '搜索车牌号',
hintStyle: TextStyle(color: Color(0xFFBBBBBB), fontSize: 14),
prefixIcon: Icon(Icons.search_sharp, color: Color(0xFFBBBBBB), size: 20),
border: InputBorder.none,
contentPadding: EdgeInsets.only(bottom: 12),
),
),
);
}
Widget _buildFilterBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: controller.statusOptions.entries.map((entry) {
return Obx(() {
bool isSelected = controller.selectedStatus.value == entry.key;
return GestureDetector(
onTap: () => controller.onStatusSelected(entry.key),
child: Container(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF006633) : Colors.white,
borderRadius: BorderRadius.circular(15),
),
child: Text(
entry.value,
style: TextStyle(
color: isSelected
? Colors.white
: Color.fromRGBO(148, 163, 184, 1),
fontSize: 12.sp,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w600,
),
),
),
);
});
}).toList(),
),
),
),
_buildTimeFilterIcon(),
],
),
);
}
Widget _buildTimeFilterIcon() {
return PopupMenuButton<String>(
icon: LoginUtil.getAssImg("ic_ex_menu@2x"),
onSelected: controller.onDateTypeSelected,
itemBuilder: (context) => [
const PopupMenuItem(value: 'week', child: Text('最近一周')),
const PopupMenuItem(value: 'month', child: Text('最近一月')),
const PopupMenuItem(value: 'three_month', child: Text('最近三月')),
],
);
}
Widget _buildSummaryCard() {
return Container(
margin: const EdgeInsets.only(left: 16, right: 16,bottom: 12),
padding: const EdgeInsets.all(20),
height: 160,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
image: const DecorationImage(
image: AssetImage('assets/images/history_bg.png'),
fit: BoxFit.cover,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('加氢站', style: TextStyle(color: Colors.white70, fontSize: 12)),
Text(
controller.stationName,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Obx(
() => Row(
children: [
_buildSummaryItem('实际加氢量', '${controller.totalHydrogen.value} Kg'),
const SizedBox(width: 40),
_buildSummaryItem('预约完成次数', '${controller.totalCompletions.value}'),
],
),
),
],
),
);
}
Widget _buildSummaryItem(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(color: Colors.white60, fontSize: 12)),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _buildHistoryList() {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.historyList.isEmpty) {
return const Center(
child: Text('暂无相关记录', style: TextStyle(color: Color(0xFF999999))),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: controller.historyList.length,
itemBuilder: (context, index) {
return _buildHistoryItem(controller.historyList[index]);
},
);
});
}
Widget _buildHistoryItem(ReservationModel item) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'车牌号',
style: TextStyle(
color: Color.fromRGBO(148, 163, 184, 1),
fontSize: 12.sp,
),
),
const SizedBox(height: 4),
Text(
item.plateNumber,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
_buildStatusBadge(item.status),
],
),
const SizedBox(height: 16),
Row(
children: [
_buildInfoColumn('加氢时间:', item.time),
_buildInfoColumn('加氢量', '${item.amount} Kg', isRight: true),
],
),
],
),
);
}
Widget _buildInfoColumn(String label, String value, {bool isRight = false}) {
return Expanded(
child: Column(
crossAxisAlignment: isRight ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(color: Color.fromRGBO(148, 163, 184, 1), fontSize: 12.sp),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: isRight ? 16 : 13,
fontWeight: isRight ? FontWeight.bold : FontWeight.normal,
color: const Color(0xFF333333),
),
),
],
),
);
}
Widget _buildStatusBadge(ReservationStatus status) {
String text = '未知';
Color bgColor = Colors.grey.shade100;
Color textColor = Colors.grey;
switch (status) {
case ReservationStatus.pending:
text = '待加氢';
bgColor = const Color(0xFFFFF7E8);
textColor = const Color(0xFFFF9800);
break;
case ReservationStatus.completed:
text = '已加氢';
bgColor = const Color(0xFFE8F5E9);
textColor = const Color(0xFF4CAF50);
break;
case ReservationStatus.rejected:
text = '拒绝加氢';
bgColor = const Color(0xFFFFEBEE);
textColor = const Color(0xFFF44336);
break;
case ReservationStatus.unadded:
text = '未加氢';
bgColor = const Color(0xFFFFEBEE);
textColor = const Color(0xFFF44336);
break;
case ReservationStatus.cancel:
text = '已取消';
bgColor = const Color(0xFFFFEBEE);
textColor = const Color(0xFFF44336);
break;
default:
text = '未知状态';
bgColor = Colors.grey;
textColor = Colors.grey;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: textColor.withOpacity(0.3)),
),
child: Text(
text,
style: TextStyle(color: textColor, fontSize: 12, fontWeight: FontWeight.bold),
),
);
}
}

View File

@@ -1,42 +1,194 @@
import 'dart:async';
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/login/view.dart';
import '../../../common/styles/theme.dart';
import '../../../storage_service.dart';
class ReservationController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'b_reservation'; // 确保ID与View中一致
String get builderId => 'b_reservation';
// --- 运营状态下拉菜单所需的状态 ---
// 下拉菜单的选项列表
final List<String> operationStatusOptions = ["营运中", "维修中", "暂停营业", "站点关闭"];
// 当前选中的值,默认为'运营中'
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
bool get listenLifecycleEvent => true;
@override
void onInit() {
super.onInit();
// 1. 初始化默认时间
customStartTime = DateTime.now();
customEndTime = customStartTime!.add(const Duration(days: 1));
renderData();
msgNotice(); // 红点消息
startAutoRefresh();
}
@override
void onPaused() {
stopAutoRefresh();
super.onPaused();
}
@override
void onClose() {
stopAutoRefresh();
broadcastTitleController.dispose();
broadcastContentController.dispose();
super.onClose();
}
void startAutoRefresh() {
// 先停止已存在的定时器,防止重复启动
stopAutoRefresh();
// 创建一个每5分钟执行一次的周期性定时器
_refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
renderData();
});
}
///停止定时器的方法
void stopAutoRefresh() {
// 如果定时器存在并且是激活状态,就取消它
_refreshTimer?.cancel();
_refreshTimer = null; // 置为null方便判断
}
String name = "";
String address = "";
String phone = "";
String costPrice = ""; //氢气价格
String customerPrice = ""; //官方价格
String costPrice = "";
String customerPrice = "";
String startBusiness = "";
String endBusiness = "";
String timeStr = "";
String operatingEnterprise = "";
String hydrogenId = "";
String jobTipStr = "";
String jobDetailsStr = "";
String jobId = "";
Timer? _refreshTimer;
bool isNotice = false;
Future<void> renderData() async {
showLoading("加载中");
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 {
final List<dynamic> dataList = jobDataResult.data is List
? jobDataResult.data
: [];
final firstJob = dataList[0];
jobId = firstJob["id"] ?? "";
String endTime = firstJob["endTime"] ?? "";
String beginTime = firstJob["beginTime"] ?? "";
String hydStatus = firstJob["hydStatus"].toString() ?? "";
String hydStatusStr = "";
if (hydStatus == "0") {
hydStatusStr = "营运中";
} else if (hydStatus == "1") {
hydStatusStr = "维修中";
} else if (hydStatus == "2") {
hydStatusStr = "站点关闭";
} else if (hydStatus == "3") {
hydStatusStr = "暂停营业";
}
//现在的时间晚于开始时间就不显示文案
bool isJobStarted = false;
try {
if (beginTime.isNotEmpty) {
DateTime beginDateTime = DateTime.parse(beginTime);
if (DateTime.now().isAfter(beginDateTime)) {
isJobStarted = true;
}
}
} catch (e) {
print("开始时间解析失败: $e");
}
if (isJobStarted) {
jobTipStr = "";
}
//结束时间
if (endTime.isNotEmpty) {
try {
// 解析时间字符串
DateTime endDateTime = DateTime.parse(endTime);
DateTime beginDateTime = DateTime.parse(beginTime);
DateTime now = DateTime.now(); //计算时间差 (endTime - now)
Duration diff = endDateTime.difference(now);
// 计算小时数 (允许小数,例如 0.5)
// inMinutes / 60 可以得到更精确的小数小时
double hoursLeft = diff.inMinutes / 60.0;
//计算当前时间-开始时间
Duration startDiff = beginDateTime.difference(now);
double hoursUntilStart = startDiff.inMinutes / 60.0;
// 只有在【当前时间早于开始时间】且【剩余时间大于0】时才显示文案
if (now.isBefore(beginDateTime) && hoursLeft > 0) {
// 如果是正数,表示还有多久结束
String timeTip = " ${hoursUntilStart.toStringAsFixed(2)}小时后";
jobTipStr = "$timeTip$hydStatusStr";
} else {
jobTipStr = "";
}
jobDetailsStr =
"当前站点已设置$beginTime至$endTime,共${hoursLeft.toStringAsFixed(2)}小时,为$hydStatusStr状态";
// 如果是处于非营运状态,自动回填开始和结束时间
// 假设 customStartTime 是现在customEndTime 是接口返回的结束时间
customStartTime = beginDateTime;
customEndTime = endDateTime;
} catch (e) {
print("时间解析失败: $e");
}
}
} catch (e) {
Logger.d("解析失败或者没返回信息: $e");
jobTipStr = "";
}
}
}
//获取站点信息
var responseData = await HttpService.to.get(
'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}',
);
@@ -49,32 +201,30 @@ class ReservationController extends GetxController with BaseControllerMixin {
try {
var result = BaseModel.fromJson(responseData.data);
name = result.data["name"];
name = result.data["name"] ?? "";
hydrogenId = result.data["hydrogenId"].toString();
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 = "暂无价格";
}
address = result.data["address"] ?? "";
phone = result.data["phone"];
startBusiness = result.data["startBusiness"];
endBusiness = result.data["endBusiness"];
var rawCostPrice = result.data["costPrice"];
costPrice = (rawCostPrice != null && rawCostPrice.toString().isNotEmpty)
? "¥$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();
String temp = result.data["siteStatusName"].toString();
selectedOperationStatus = (temp.isEmpty || temp == "null") ? "营运中" : temp;
if (startBusiness.isNotEmpty && endBusiness.isNotEmpty) {
// 营业时间为24小时的特殊处理
if (startBusiness == "00:00:00" && endBusiness == "23:59:59") {
timeStr = "24小时营业";
} else {
@@ -86,19 +236,39 @@ class ReservationController extends GetxController with BaseControllerMixin {
operatingEnterprise = operatingEnterprise.isEmpty ? "暂未设置" : operatingEnterprise;
updateUi();
dismissLoading();
} catch (e) {
// 如果解析 JSON 失败
dismissLoading();
showToast('数据异常');
}
} catch (e) {
dismissLoading();
} finally {
updateUi();
}
}
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
/// 更新运营状态的方法
void onOperationStatusChanged(String? newValue) {
if (newValue != null) {
selectedOperationStatus = newValue;
@@ -106,14 +276,105 @@ 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 {
var responseData = await HttpService.to.post(
'appointment/station/updateStationStatus',
data: {
'hydrogenId': hydrogenId,
'name': name,
'siteStatus': selectedOperationStatus == "营运中"
? "0"
: selectedOperationStatus == "维修中"
@@ -123,18 +384,27 @@ class ReservationController extends GetxController with BaseControllerMixin {
: selectedOperationStatus == "暂停营业"
? "3"
: 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(),
},
);
if (responseData == null && responseData!.data == null) {
if (responseData == null || responseData.data == null) {
dismissLoading();
showToast('服务暂不可用,请稍后');
return;
}
var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) {
showSuccessToast("保存成功");
showSuccessToast("保存成功,已同步通知对应司机");
//重新刷新页面
renderData();
}
dismissLoading();
} catch (e) {
@@ -142,11 +412,87 @@ class ReservationController extends GetxController with BaseControllerMixin {
}
}
void logout() async {
// TODO: 在这里执行退出登录的逻辑
//清理本地缓存的用户信息 导航到登录页面
await StorageService.to.clearLoginInfo();
/// 显示当前未执行任务的详情弹窗
void showJob() {
if (jobDetailsStr.isEmpty) {
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());
}
}

View File

@@ -1,11 +1,17 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter/services.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/b_page/reservation/controller.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
class ReservationPage extends GetView<ReservationController> {
const ReservationPage({super.key});
// 定义主题色
static const kPrimaryColor = Color(0xFF006D35); // 效果图深绿色
static const kBgColor = Color(0xFFF5F7F9); // 背景灰
@override
Widget build(BuildContext context) {
return GetBuilder<ReservationController>(
@@ -13,21 +19,28 @@ class ReservationPage extends GetView<ReservationController> {
id: 'b_reservation',
builder: (_) {
return Scaffold(
backgroundColor: kBgColor,
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeaderCard(),
const SizedBox(height: 12),
_buildInfoFormCard(context),
const SizedBox(height: 12),
_buildTipsCard(),
const SizedBox(height: 12),
_buildLogoutButton(),
],
),
child: Column(
children: [
_buildTopSection(context),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
children: [
SizedBox(height: 16),
_buildBasicInfoCard(),
SizedBox(height: 16),
_buildOperationContentCard(context),
SizedBox(height: 16.h),
_buildSystemTips(),
SizedBox(height: 24),
_buildLogoutButton(),
SizedBox(height: 95.h),
],
),
),
],
),
),
);
@@ -35,126 +48,149 @@ class ReservationPage extends GetView<ReservationController> {
);
}
/// 构建顶部的站点信息头卡片
Widget _buildHeaderCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
/// 1. 顶部个人信息及统计栏
Widget _buildTopSection(BuildContext context) {
return Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(30)),
),
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 10,
left: 20,
right: 20,
bottom: 25,
),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.local_gas_station, color: Colors.blue, size: 40),
title: Text(
controller.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
subtitle: Text(controller.address),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(12),
Row(
children: [
CircleAvatar(
radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
),
child: Text(
controller.selectedOperationStatus,
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
fontSize: 12,
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
controller.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
_buildStatusTag(),
],
),
const SizedBox(height: 4),
Text(
"站点:${controller.address}",
style: TextStyle(color: Colors.grey[500], fontSize: 13),
),
],
),
),
),
IconButton(
onPressed: () async{
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
],
),
const Divider(height: 1, indent: 16, endIndent: 16),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildHeaderStat(controller.costPrice, '氢气价格'),
_buildHeaderStat(controller.timeStr, '营业时间'),
_buildHeaderStat('98%', '设备状态'),
],
),
const SizedBox(height: 25),
Row(
children: [
_buildStatBox("氢气价格", "Hydrogen price", controller.customerPrice, "/kg"),
SizedBox(width: 4.w),
_buildStatBox("营业时间", "Opening time", controller.timeStr, ""),
SizedBox(width: 4.w),
_buildStatBox("设备状态", "Anlagenzustand", "98", "%"),
],
),
],
),
);
}
/// 构建头部卡片中的单个统计项
Widget _buildHeaderStat(String value, String label) {
return Column(
children: [
Text(
value,
style: const TextStyle(
color: Colors.blue,
fontSize: 20,
fontWeight: FontWeight.bold,
),
Widget _buildStatusTag() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFE1F5FE),
borderRadius: BorderRadius.circular(10),
),
child: Text(
controller.selectedOperationStatus,
style: TextStyle(
color: Color(0xFF03A9F4),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
],
),
);
}
/// 构建包含所有信息表单的卡片
Widget _buildInfoFormCard(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Padding(
padding: const EdgeInsets.all(16.0),
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
return Expanded(
child: Container(
padding: EdgeInsets.only(left: 12.w, top: 4.h, bottom: 4.h),
decoration: BoxDecoration(
color: kBgColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- 基本信息 ---
_buildSectionTitle('基本信息'),
_buildDisplayField(label: '站点名称', value: controller.name),
_buildDisplayField(label: '运营企业', value: controller.operatingEnterprise),
_buildDisplayField(label: '站点地址', value: controller.address),
const SizedBox(height: 16),
// --- 价格信息 ---
_buildSectionTitle('价格信息'),
_buildDisplayField(label: '氢气价格 (元/kg)', value: controller.costPrice),
_buildDisplayField(label: '官方价格 (元/kg)', value: controller.customerPrice),
const SizedBox(height: 16),
// --- 运营信息 ---
_buildSectionTitle('运营信息'),
Text('运营状态', style: TextStyle(color: Colors.grey[600], fontSize: 14)),
Text(
title,
style: TextStyle(
fontSize: 12.sp,
color: Color.fromRGBO(51, 51, 51, 0.8),
fontWeight: FontWeight.w400,
),
),
Text(enTitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8),
//下拉选择框
DropdownButtonFormField<String>(
value: controller.selectedOperationStatus,
items: controller.operationStatusOptions.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: controller.onOperationStatusChanged,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
),
),
const SizedBox(height: 12),
_buildDisplayField(label: '营业时间', value: controller.timeStr),
_buildDisplayField(label: '联系电话', value: controller.phone),
const SizedBox(height: 24),
//保存按钮
ElevatedButton(
onPressed: controller.saveInfo,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('保存信息', style: TextStyle(fontSize: 16)),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color(0xFF333333),
),
),
const SizedBox(width: 2),
Text(unit, style: const TextStyle(fontSize: 11, color: Colors.grey)),
],
),
],
),
@@ -162,72 +198,264 @@ class ReservationPage extends GetView<ReservationController> {
);
}
/// 构建静态提示信息卡片
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'),
],
),
/// 2. 站点基本信息
Widget _buildBasicInfoCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
);
}
/// 构建退出登录按钮
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 _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(width: 4, height: 16, color: Colors.blue),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text(
"站点基本信息",
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 15),
_buildInfoRow("站点名称", controller.name),
_buildInfoRow("运营企业", controller.operatingEnterprise),
_buildInfoRow("站点地址", controller.address),
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(color: Colors.grey, fontSize: 11.sp),
),
Text(
value,
style: TextStyle(
color: Color(0xFF333333),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
/// 3. 运营信息/站点广播 Tab 及内容
Widget _buildOperationContentCard(BuildContext context) {
return GestureDetector(
onTap: hideKeyboard,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
// 自定义 TabBar
Obx(
() => Padding(
padding: const EdgeInsets.only(left: 16, top: 16),
child: Row(
children: [
_buildTabTitle(0, "运营信息"),
const SizedBox(width: 30),
_buildTabTitle(1, "站点广播"),
],
),
),
),
Obx(
() => controller.selectedTabIndex.value == 0
? _buildOperatingForm(context)
: _buildBroadcastForm(),
),
],
),
),
);
}
Widget _buildTabTitle(int index, String title) {
bool isSelected = controller.selectedTabIndex.value == index;
return GestureDetector(
onTap: () => controller.selectedTabIndex.value = index,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.black87 : Colors.grey,
),
),
if (isSelected)
Container(
margin: const EdgeInsets.only(top: 4),
width: 25,
height: 3,
decoration: BoxDecoration(
color: const Color(0xFF00A870), // 效果图中的亮绿色横线
borderRadius: BorderRadius.circular(2),
),
),
],
),
);
}
Widget _buildOperatingForm(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'运营状态',
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
//加氢站未执行的状态修改任务
if (controller.jobTipStr.isNotEmpty)
GestureDetector(
onTap: controller.showJob,
child: Row(
children: [
Text(
controller.jobTipStr,
style: TextStyle(color: Colors.yellow[800], fontSize: 14),
),
SizedBox(width: 2.w),
Icon(AntdIcon.info_circle, size: 14, color: Colors.yellow[800]),
],
),
),
],
),
const SizedBox(height: 12),
// 状态网格选择
Wrap(
spacing: 4,
runSpacing: 4,
children: controller.operationStatusOptions.map((status) {
bool isSelected = controller.selectedOperationStatus == status;
return GestureDetector(
onTap: () => controller.onOperationStatusChanged(status),
child: Container(
width: (Get.width - 80) / 2,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? kPrimaryColor : const Color(0xFFEBEBEB),
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
status,
style: TextStyle(
fontSize: 13.sp,
color: isSelected ? Colors.white : Color.fromRGBO(51, 51, 51, 1),
fontWeight: FontWeight.w500,
),
),
),
);
}).toList(),
),
SizedBox(height: 12.h),
if (controller.selectedOperationStatus == "营运中")
_buildDisplayField(label: '营业时间', value: controller.timeStr)
else
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInputLabel("开始时间"),
_buildDateTimePicker(
controller.customStartTimeStr,
() => controller.pickDateTime(context, true),
),
const SizedBox(height: 15),
_buildInputLabel("结束时间"),
_buildDateTimePicker(
controller.customEndTimeStr,
() => controller.pickDateTime(context, false),
),
const SizedBox(height: 15),
],
),
_buildDisplayField(label: '联系电话', value: controller.phone),
const SizedBox(height: 25),
Row(
children: [
Expanded(
flex: 1,
child: OutlinedButton(
onPressed: () {
controller.renderData();
}, // 重置逻辑
style: OutlinedButton.styleFrom(
side: const BorderSide(color: kPrimaryColor),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text("重置", style: TextStyle(color: kPrimaryColor)),
),
),
const SizedBox(width: 15),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: controller.saveInfo,
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text("保存设置", style: TextStyle(color: Colors.white)),
),
),
],
),
],
),
);
}
/// 构建一个“标签+纯文本”的显示行
Widget _buildDisplayField({required String label, required String value}) {
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)),
Text(
label,
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
decoration: BoxDecoration(
color: Colors.grey[200], // 使用灰色背景以区分
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8.0),
border: Border.all(color: Colors.grey[300]!),
),
@@ -241,16 +469,177 @@ class ReservationPage extends GetView<ReservationController> {
);
}
/// 构建带图标的提示信息行
Widget _buildInfoItem(IconData icon, String text) {
return Row(
children: [
Icon(icon, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 14, color: Colors.black54)),
Widget _buildBroadcastForm() {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInputLabel("通知标题"),
TextField(
controller: controller.broadcastTitleController,
decoration: _inputDecoration("例如:临时闭站通知"),
),
const SizedBox(height: 15),
_buildInputLabel("通知内容"),
TextField(
controller: controller.broadcastContentController,
maxLines: 4,
decoration: _inputDecoration("请输入通知内容..."),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
flex: 1,
child: OutlinedButton(
onPressed: () {
controller.broadcastTitleController.clear();
controller.broadcastContentController.clear();
}, // 重置逻辑
style: OutlinedButton.styleFrom(
side: const BorderSide(color: kPrimaryColor),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text("重置", style: TextStyle(color: kPrimaryColor)),
),
),
const SizedBox(width: 15),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: controller.sendBroadcast,
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: const Text("发送广播", style: TextStyle(color: Colors.white)),
),
),
],
),
],
),
);
}
Widget _buildInputLabel(String label) {
return Padding(
padding: const EdgeInsets.only(left: 0, bottom: 8),
child: Text(
label,
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
],
),
);
}
Widget _buildDateTimePicker(String value, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFF00A870).withOpacity(0.5)),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(value, style: const TextStyle(color: Colors.black87)),
const Icon(Icons.calendar_today_outlined, size: 18, color: Color(0xFF00A870)),
],
),
),
);
}
InputDecoration _inputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
);
}
/// 4. 系统提醒
Widget _buildSystemTips() {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: const Color(0xFFF1F9F6), // 极浅绿色背景
borderRadius: BorderRadius.circular(10),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline, color: Color.fromRGBO(1, 113, 55, 1), size: 20),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"系统提醒",
style: TextStyle(
color: Color.fromRGBO(1, 113, 55, 1),
fontWeight: FontWeight.bold,
fontSize: 14.sp,
),
),
SizedBox(height: 6.h),
Text(
"请您确保所提供的信息准确无误,价格信息也将实时\n更新至用户端",
style: TextStyle(color: Color.fromRGBO(1, 113, 55, 0.8), fontSize: 12.sp),
),
SizedBox(height: 6.h),
Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(color: Color.fromRGBO(1, 113, 55, 0.8), fontSize: 12.sp),
),
],
),
],
),
);
}
/// 5. 退出登录按钮
Widget _buildLogoutButton() {
return SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: controller.logout,
style: ElevatedButton.styleFrom(
backgroundColor: Color.fromRGBO(204, 52, 46, 1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
elevation: 0,
),
child: const Text(
"退出登录",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
}
}

View File

@@ -1,20 +1,27 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:getx_scaffold/getx_scaffold.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:pull_to_refresh/pull_to_refresh.dart';
enum ReservationStatus {
pending, // 待处理 ( addStatus: 1)
completed, // 完成 ( addStatus: 2)
rejected, // 拒绝 ( 3)
pending, // 待处理 ( addStatus: 0)
completed, // 完成 ( addStatus: 1)
rejected, // 拒绝 ( -1)
unadded, // 未加 ( 2)
cancel, // 取消预约
unknown, // 未知状态
}
class ReservationModel {
final String id;
final String stationId;
final String plateNumber;
final String amount;
String amount;
final String time;
final String contactPerson;
final String contactPhone;
@@ -22,6 +29,7 @@ class ReservationModel {
final String contacts;
final String phone;
final String rejectReason;
final String stationName;
final String startTime;
final String endTime;
@@ -31,14 +39,17 @@ class ReservationModel {
final String stateName;
final String addStatus;
final String addStatusName;
bool hasEdit;
ReservationModel({
required this.id,
required this.stationId,
required this.plateNumber,
required this.amount,
required this.time,
required this.contactPerson,
required this.contactPhone,
required this.hasEdit,
this.status = ReservationStatus.pending,
required this.contacts,
required this.phone,
@@ -51,38 +62,49 @@ class ReservationModel {
required this.stateName,
required this.addStatus,
required this.addStatusName,
required this.rejectReason,
});
/// 工厂构造函数用于从JSON创建ReservationModel实例
factory ReservationModel.fromJson(Map<String, dynamic> json) {
//1完成 0待处理 2已拒绝
// 1完成 2未加 -1拒绝 0是待加氢
ReservationStatus currentStatus;
int statusFromServer = json['addStatus'] as int? ?? 0;
switch (statusFromServer) {
case 0:
currentStatus = ReservationStatus.pending;
break;
case 1:
currentStatus = ReservationStatus.completed;
break;
case 2:
currentStatus = ReservationStatus.rejected;
break;
default:
currentStatus = ReservationStatus.unknown;
int state = json['state'] as int? ?? 0;
if (state == -1) {
currentStatus = ReservationStatus.rejected;
} else {
switch (statusFromServer) {
case 0:
currentStatus = ReservationStatus.pending;
break;
case 1:
currentStatus = ReservationStatus.completed;
break;
case 2:
currentStatus = ReservationStatus.unadded;
break;
case 6:
currentStatus = ReservationStatus.cancel;
break;
default:
currentStatus = ReservationStatus.unknown;
}
}
// 格式化时间显示
String startTimeStr = json['startTime']?.toString() ?? '';
String endTimeStr = json['endTime']?.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
: '时间未定';
return ReservationModel(
// 原始字段用于UI兼容
id: json['id']?.toString() ?? '',
stationId: json['stationId']?.toString() ?? '',
plateNumber: json['plateNumber']?.toString() ?? '未知车牌',
amount: '${json['hydAmount']?.toString() ?? '0'}kg',
time: timeRange,
@@ -102,6 +124,8 @@ class ReservationModel {
addStatus: statusFromServer.toString(),
addStatusName: json['addStatusName']?.toString() ?? '',
stateName: json['stateName']?.toString() ?? '',
rejectReason: json['rejectReason']?.toString() ?? '',
hasEdit: true,
);
}
}
@@ -119,31 +143,68 @@ class SiteController extends GetxController with BaseControllerMixin {
List<ReservationModel> reservationList = [];
Timer? _refreshTimer;
final TextEditingController searchController = TextEditingController();
bool isNotice = false;
final RefreshController refreshController = RefreshController(initialRefresh: false);
@override
bool get listenLifecycleEvent => true;
@override
void onInit() {
super.onInit();
renderData();
msgNotice();
startAutoRefresh();
}
@override
void onPaused() {
stopAutoRefresh();
super.onPaused();
}
@override
void onClose() {
stopAutoRefresh();
searchController.dispose();
super.onClose();
}
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
void startAutoRefresh() {
// 先停止已存在的定时器,防止重复启动
stopAutoRefresh();
// 创建一个每5分钟执行一次的周期性定时器
_refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
fetchReservationData();
renderData();
});
}
/// 【6. 新增】停止定时器的方法
void onRefresh() => renderData(isRefresh: true);
///停止定时器的方法
void stopAutoRefresh() {
// 如果定时器存在并且是激活状态,就取消它
_refreshTimer?.cancel();
@@ -155,13 +216,17 @@ class SiteController extends GetxController with BaseControllerMixin {
Future<void> fetchReservationData() async {
showLoading("加载中");
final String searchText = searchController.text.trim();
try {
var response = await HttpService.to.post(
"appointment/orderAddHyd/sitOrderPage",
data: {
'stationName': name, // 使用从 renderData 中获取到的 name
'pageNum': 1,
'pageSize': 30, // 暂时不考虑分页一次获取30条
'pageSize': 50, // 暂时不考虑分页一次获取30条
'keyword': searchText, // 加氢站名称、手机号
'stationId': StorageService.to.userId,
},
);
@@ -170,6 +235,7 @@ class SiteController extends GetxController with BaseControllerMixin {
showToast('暂时无法获取预约数据');
hasReservationData = false;
reservationList = [];
dismissLoading();
return;
}
@@ -179,7 +245,7 @@ class SiteController extends GetxController with BaseControllerMixin {
// 【核心修改】处理接口返回的列表数据
final dataMap = baseModel.data as Map<String, dynamic>;
final List<dynamic> listFromServer = dataMap['list'] ?? [];
final List<dynamic> listFromServer = dataMap['records'] ?? [];
// 使用 .map() 遍历列表,将每个 item 转换为一个 ReservationModel 对象
reservationList = listFromServer.map((item) {
@@ -208,38 +274,290 @@ class SiteController extends GetxController with BaseControllerMixin {
/// 确认预约
Future<void> confirmReservation(String id) async {
final item = reservationList.firstWhere((item) => item.id == id);
DialogX.to.showConfirmDialog(
title: '确认预约',
message: '确定要确认车牌号${item.plateNumber}的预约吗',
onConfirm: () {
upDataService(id, 1, item);
},
onCancel: () {},
final item = reservationList.firstWhere(
(item) => item.id == id,
orElse: () => throw Exception('Reservation not found'),
);
final TextEditingController amountController = TextEditingController(
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);
},
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 {
final item = reservationList.firstWhere((item) => item.id == id);
DialogX.to.showConfirmDialog(
title: '拒绝预约',
message: '确定要拒绝车牌号${item.plateNumber}的预约吗',
onConfirm: () {
upDataService(id, 2, item);
},
onCancel: () {},
final TextEditingController reasonController = TextEditingController();
final RxString selectedReason = ''.obs;
// 预设的拒绝原因列表
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(); // 选择预设原因时,清空自定义输入
}
},
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("确认中");
try {
var responseData = await HttpService.to.post(
'appointment/orderAddHyd/completeOrder',
data: {'id': id, 'addStatus': status},
);
var responseData;
if (addStatus == -1) {
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) {
dismissLoading();
@@ -252,12 +570,16 @@ class SiteController extends GetxController with BaseControllerMixin {
}
dismissLoading();
if (status == 1) {
//1完成 2未加 -1拒绝
if (addStatus == 1) {
item.status = ReservationStatus.completed;
} else if (status == 2) {
item.amount = "${addHydAmount}kg";
} else if (addStatus == -1) {
item.status = ReservationStatus.rejected;
} else if (addStatus == 2) {
item.status = ReservationStatus.unadded;
}
updateUi();
renderData();
} catch (e) {
dismissLoading();
}
@@ -267,8 +589,10 @@ class SiteController extends GetxController with BaseControllerMixin {
String orderAmount = "";
String completedAmount = "";
String name = "";
String orderTotalAmount = "";
String orderUnfinishedAmount = "";
Future<void> renderData() async {
Future<void> renderData({bool isRefresh = false}) async {
try {
var responseData = await HttpService.to.get(
'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}',
@@ -286,14 +610,25 @@ class SiteController extends GetxController with BaseControllerMixin {
orderAmount = result.data["orderAmount"].toString();
completedAmount = result.data["completedAmount"].toString();
name = result.data["name"].toString();
orderTotalAmount = result.data["orderTotalAmount"].toString();
orderUnfinishedAmount = result.data["orderUnfinishedAmount"].toString();
leftHydrogen = leftHydrogen.isEmpty ? "统计中" : leftHydrogen.toString();
//加载列表数据
fetchReservationData();
orderTotalAmount = orderTotalAmount.isEmpty ? "统计中" : orderTotalAmount.toString();
orderUnfinishedAmount = orderUnfinishedAmount.isEmpty
? "统计中"
: orderUnfinishedAmount.toString();
} catch (e) {
showToast('数据异常');
}
} catch (e) {}
} catch (e) {
} finally {
//加载列表数据
fetchReservationData();
if (isRefresh) {
refreshController.refreshCompleted();
}
}
}
}

View File

@@ -1,6 +1,10 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/history/view.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'controller.dart';
@@ -14,187 +18,347 @@ class SitePage extends GetView<SiteController> {
init: SiteController(),
id: 'site',
builder: (_) {
return SingleChildScrollView(child: _buildView());
return Scaffold(
backgroundColor: Color.fromRGBO(247, 249, 251, 1),
body: SmartRefresher(
controller: controller.refreshController,
enablePullUp: false,
onRefresh: controller.onRefresh,
child: SingleChildScrollView(child: _buildView(context)),
),
);
},
);
}
// 主视图
Widget _buildView() {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 第一个卡片: 今日预约统计
Card(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Column(
Widget _buildView(BuildContext context) {
return Column(
children: [
// 第一个卡片: 今日预约统计
_buildTopSection(context),
Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.only(top: 17.h, bottom: 10.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"预约信息",
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontWeight: FontWeight.w600,
fontSize: 14.sp,
),
),
GestureDetector(
onTap: () {
Get.to(
() => HistoryPage(),
arguments: {'stationName': controller.name},
);
},
child: Text(
'历史记录',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: Color.fromRGBO(156, 163, 175, 1),
),
),
),
],
),
),
// 第二个卡片: 预约信息
Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
const Icon(Icons.calendar_today, color: Colors.blue, size: 32),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'今日预约统计',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
"Today's Reservation Statistics",
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'实时',
style: TextStyle(
color: Colors.blue,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const SizedBox(height: 10),
const Divider(height: 1, indent: 16, endIndent: 16),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(controller.leftHydrogen, '剩余余量'),
_buildStatItem(controller.orderAmount, '预约车辆'),
_buildStatItem(controller.completedAmount, '已完成'),
],
),
),
_buildSearchView(),
SizedBox(height: 15.h),
controller.hasReservationData
? _buildReservationListView()
: _buildEmptyReservationView(),
],
),
),
),
// 第二个卡片: 预约信息
Card(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
margin: const EdgeInsets.only(bottom: 12),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Container(
color: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Row(
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'今日预约信息',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 2),
Text(
'Reservation Information',
style: TextStyle(fontSize: 12, color: Colors.white70),
),
],
),
),
ElevatedButton.icon(
onPressed: () {
controller.fetchReservationData();
},
icon: const Icon(Icons.refresh, size: 16),
label: const Text('刷新'),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.blue,
backgroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
SizedBox(height: 35.h),
//第三部分
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: const Color(0xFFF1F9F6), // 极浅绿色背景
borderRadius: BorderRadius.circular(10),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline,
color: Color.fromRGBO(1, 113, 55, 1),
size: 20,
),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"系统提醒",
style: TextStyle(
color: Color.fromRGBO(1, 113, 55, 1),
fontWeight: FontWeight.bold,
fontSize: 14.sp,
),
),
),
],
SizedBox(height: 6.h),
Text(
"数据每五分钟自动刷新,如需实时更新请下拉页面",
style: TextStyle(
color: Color.fromRGBO(1, 113, 55, 0.8),
fontSize: 12.sp,
),
),
SizedBox(height: 6.h),
Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(
color: Color.fromRGBO(1, 113, 55, 0.8),
fontSize: 12.sp,
),
),
],
),
],
),
),
],
),
),
SizedBox(height: 105.h),
],
);
}
Widget _buildTopSection(BuildContext context) {
return Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)),
),
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 10,
left: 20,
right: 20,
bottom: 25,
),
child: Column(
children: [
Row(
children: [
CircleAvatar(
radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
"今日预约统计",
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w400),
),
const SizedBox(width: 8),
],
),
const SizedBox(height: 4),
Text(
"Today's Reservation Statistics",
style: TextStyle(color: Colors.grey[500], fontSize: 13),
),
],
),
),
IconButton(
onPressed: () async {
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
controller.hasReservationData
? _buildReservationListView()
: _buildEmptyReservationView(),
],
),
),
//第三部分
Card(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoItem(Icons.info_outline, '数据每5分钟自动刷新一次'),
const SizedBox(height: 8),
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
],
),
),
],
),
const SizedBox(height: 25),
Row(
children: [
_buildStatBox("剩余氢量", "remaining quantity", controller.leftHydrogen, "kg"),
SizedBox(width: 4.w),
_buildStatBox(
"今日加氢量",
"Have been added",
controller.orderTotalAmount,
"kg",
),
SizedBox(width: 4.w),
_buildStatBox(
"未加氢总量",
"No quantity added",
controller.orderUnfinishedAmount,
"kg",
),
],
),
],
),
);
}
/// 构建单个统计项
Widget _buildStatItem(String value, String label, {Color valueColor = Colors.blue}) {
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
child: Container(
padding: EdgeInsets.only(left: 12.w, top: 4.h, bottom: 4.h),
decoration: BoxDecoration(
color: Color(0xFFF5F7F9),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 12.sp,
color: Color.fromRGBO(51, 51, 51, 0.8),
fontWeight: FontWeight.w400,
),
),
Text(enTitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color(0xFF333333),
),
),
const SizedBox(width: 2),
Text(unit, style: const TextStyle(fontSize: 11, color: Colors.grey)),
],
),
],
),
),
);
}
//搜索输入框,提示可以输入车牌或者手机
Widget _buildSearchView() {
return Padding(
padding: EdgeInsets.fromLTRB(0, 0, 0, 8.h),
child: Row(
children: [
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: valueColor,
Expanded(
child: SizedBox(
height: 42.h,
child: TextField(
textAlignVertical: TextAlignVertical.center,
controller: controller.searchController, // 绑定控制器
decoration: InputDecoration(
filled: true,
isDense: true,
fillColor: Colors.white,
hintText: '输入车牌号或手机号搜索...',
contentPadding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color.fromRGBO(229, 231, 235, 1)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color.fromRGBO(229, 231, 235, 1)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Color.fromRGBO(229, 231, 235, 1),
width: 1.5,
),
),
// 清除按钮
suffix: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
controller.searchController.clear();
controller.fetchReservationData(); // 清除后也刷新一次
},
),
GestureDetector(
onTap: () {
// 点击“搜索”按钮时触发
FocusScope.of(Get.context!).unfocus(); // 收起键盘
controller.fetchReservationData();
},
child: Container(
padding: EdgeInsets.only(
left: 16.w,
right: 16.h,
top: 4.5.h,
bottom: 4.5.h,
),
decoration: BoxDecoration(
color: Color.fromRGBO(1, 113, 55, 1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
"搜索",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 12.sp,
),
),
),
),
],
),
),
onSubmitted: (value) {
// 用户在键盘上点击“完成”或“搜索”时触发
controller.fetchReservationData();
},
),
),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
],
),
);
@@ -209,14 +373,17 @@ class SitePage extends GetView<SiteController> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inventory_2_outlined, size: 80, color: Colors.grey[300]),
const SizedBox(height: 16),
const Text('暂无预约数据', style: TextStyle(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 8),
const Text(
'点击右上角刷新按钮获取最新数据',
style: TextStyle(fontSize: 14, color: Colors.grey),
LoginUtil.getAssImg("ic_no_data@2x"),
SizedBox(height: 16.h),
Text(
'暂无订单',
style: TextStyle(
fontSize: 16.sp,
color: Colors.black54,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
],
),
);
@@ -224,152 +391,321 @@ class SitePage extends GetView<SiteController> {
/// 构建“有预约数据”的列表视图
Widget _buildReservationListView() {
return Container(
color: Colors.white,
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
// 因为外层已有滚动,这里禁用内部滚动
itemCount: controller.reservationList.length,
padding: const EdgeInsets.all(12.0),
itemBuilder: (context, index) {
final item = controller.reservationList[index];
// 调用新的方法来构建每一项
return _buildReservationItem(index, item);
},
separatorBuilder: (context, index) => const SizedBox(height: 12), // 列表项之间的间距
),
return ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
// 因为外层已有滚动,这里禁用内部滚动
itemCount: controller.reservationList.length,
itemBuilder: (context, index) {
final item = controller.reservationList[index];
// 调用新的方法来构建每一项
return _buildReservationItem(index, item);
},
);
}
Widget _buildReservationItem(int index, ReservationModel item) {
const kPrimaryGreen = Color(0xFF006D35); // 深绿
const kLineColor = Color(0xFFE0E6ED); // 线条颜色
return IntrinsicHeight(
// 保证左侧竖线能跟右侧内容高度对齐
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. 左侧样式区域(竖线 + 圆点)
SizedBox(
width: 20,
child: Column(
children: [
// 顶部的装饰圆点
Container(
margin: EdgeInsets.only(top: 3.w),
width: 8,
height: 8,
decoration: BoxDecoration(
color: kPrimaryGreen.withOpacity(0.5),
shape: BoxShape.circle,
),
),
// 延伸的竖线
Expanded(child: Container(width: 1, color: kLineColor)),
],
),
),
// 2. 右侧内容区域(时间 + 卡片数据)
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 10.w, bottom: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 时间显示
Text(
item.time, // 格式如 "09:00 - 10:00"
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
const SizedBox(height: 10),
// 数据卡片
_buildInfoCard(item),
],
),
),
),
],
),
);
}
/// 右侧具体数据卡片
Widget _buildInfoCard(ReservationModel item) {
return Container(
padding: EdgeInsets.only(left: 16.w, top: 8.5, bottom: 8.5, right: 16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.0),
border: Border.all(color: Colors.grey[200]!, width: 1.0),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF0F0F0)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 2),
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 顶部:序号、车牌号、状态
Padding(
padding: const EdgeInsets.fromLTRB(8, 12, 16, 12),
child: Row(
children: [
// 序号
Container(
width: 24,
height: 24,
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: Text(
"${index + 1}",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
// 车牌与状态标签
Row(
children: [
Text(
item.plateNumber,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
const SizedBox(width: 8),
const Icon(Icons.directions_car, color: Colors.black54),
const SizedBox(width: 4),
// 车牌号
Expanded(
child: Text(
item.plateNumber,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.themeColor,
),
),
),
// 状态标签
_buildStatusChip(item.status),
],
),
),
const Divider(height: 1),
// 中部:详细信息
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Column(
children: [
_buildDetailRow(
Icons.local_gas_station,
'加氢量',
item.amount,
valueColor: Colors.red,
),
const SizedBox(height: 8),
_buildDetailRow(Icons.access_time, '预约时间', item.time),
const SizedBox(height: 8),
_buildDetailRow(Icons.person, '联系人', item.contactPerson),
const SizedBox(height: 8),
_buildDetailRow(
Icons.phone,
'联系电话',
item.contactPhone,
valueColor: AppTheme.themeColor,
),
],
),
),
// 底部:操作按钮 (仅在待处理时显示)
if (item.status == ReservationStatus.pending)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => controller.confirmReservation(item.id),
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
child: const Text('确定'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () => controller.rejectReservation(item.id),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('拒绝'),
),
),
],
),
const SizedBox(width: 8),
_buildStatusTag(item.status),
Spacer(),
Text(
"预约量:${item.amount}",
style: TextStyle(
color: Color(0xFF00A870),
fontSize: 13.sp,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
// 联系信息
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"${item.contactPerson} | ${item.contactPhone}",
style: TextStyle(
color: Color(0xFF999999),
fontSize: 13.sp,
fontWeight: FontWeight.w400,
),
),
],
),
//操作按钮(仅在待处理状态显示)
if (item.status == ReservationStatus.pending) ...[
const SizedBox(height: 15),
const Divider(height: 1, color: Color(0xFFF5F5F5)),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildSmallButton(
"拒绝",
isOutline: true,
onTap: () {
controller.rejectReservation(item.id);
},
),
const SizedBox(width: 12),
_buildSmallButton(
"确认",
isOutline: false,
onTap: () {
controller.confirmReservation(item.id);
},
),
],
),
],
],
),
);
}
/// 通用小按钮
Widget _buildSmallButton(
String text, {
required bool isOutline,
required VoidCallback onTap,
}) {
const kPrimaryGreen = Color(0xFF006D35);
const kDangerRed = Color(0xFFFF7D7D);
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: isOutline ? Colors.white : kPrimaryGreen,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isOutline ? kDangerRed : kPrimaryGreen),
),
child: Text(
text,
style: TextStyle(
color: isOutline ? kDangerRed : Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
);
}
/// 状态标签构建(带圆角和浅色背景)
Widget _buildStatusTag(ReservationStatus status) {
Color textColor;
Color bgColor;
String text;
switch (status) {
case ReservationStatus.pending:
text = "待加氢";
textColor = const Color(0xFFFE9E62); // 橘色
bgColor = const Color(0xFFFFF2E9);
break;
case ReservationStatus.completed: // 假设已加氢状态
text = "已加氢";
textColor = const Color(0xFF00A870); // 绿色
bgColor = const Color(0xFFE6F7F1);
break;
case ReservationStatus.rejected:
text = "拒绝加氢";
textColor = const Color(0xFFFF7D7D); // 红色
bgColor = const Color(0xFFFFEEEE);
break;
case ReservationStatus.unadded:
text = '未加氢';
textColor = const Color(0xFFFF7D7D); // 红色
bgColor = const Color(0xFFFFEEEE);
break;
case ReservationStatus.cancel:
text = '已取消';
textColor = const Color(0xFFFF7D7D); // 红色
bgColor = const Color(0xFFFFEEEE);
break;
default:
text = "未知";
textColor = Colors.grey;
bgColor = Colors.grey[100]!;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: textColor.withOpacity(0.3)),
),
child: Text(
text,
style: TextStyle(color: textColor, fontSize: 11, fontWeight: FontWeight.bold),
),
);
}
/// 右侧操作按钮(拒绝/确认)
Widget _buildActionButtons(ReservationModel item) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// 拒绝按钮(空心)
GestureDetector(
onTap: () => controller.rejectReservation(item.id),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFF7D7D)),
),
child: const Text(
"拒绝",
style: TextStyle(
color: Color(0xFFFF7D7D),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 8),
// 确认按钮(实心深绿)
GestureDetector(
onTap: () => controller.confirmReservation(item.id),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF006D35),
borderRadius: BorderRadius.circular(10),
),
child: const Text(
"确认",
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}
/// 构建状态标签
Widget _buildStatusChip(ReservationStatus status) {
String text;
Color color;
switch (status) {
case ReservationStatus.pending:
text = '处理';
text = '加氢';
color = Colors.orange;
break;
case ReservationStatus.completed:
text = '完成';
text = '加氢';
color = Colors.greenAccent;
break;
case ReservationStatus.rejected:
text = '拒绝';
text = '拒绝加氢';
color = Colors.red;
break;
case ReservationStatus.unadded:
text = '未加氢';
color = Colors.red;
break;
case ReservationStatus.cancel:
text = '已取消';
color = Colors.red;
break;
default:

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/common/index.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/c_page/car_info/view.dart';
import 'package:ln_jq_app/pages/c_page/mall/mall_view.dart';
import 'package:ln_jq_app/pages/c_page/map/view.dart';
import 'package:ln_jq_app/pages/c_page/mine/view.dart';
import 'package:ln_jq_app/pages/c_page/reservation/view.dart';
@@ -9,9 +11,10 @@ import 'package:ln_jq_app/pages/c_page/reservation/view.dart';
import 'index.dart';
class BaseWidgetsPage extends GetView<BaseWidgetsController> {
BaseWidgetsPage({super.key});
BaseWidgetsPage({super.key});
final PageController _pageController = PageController();
// 主视图
Widget _buildView() {
return PageView(
@@ -20,57 +23,70 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
onPageChanged: (index) {
jumpTabAndPage(index);
},
children: _buildPages(), // 页面的列表
children: _buildPages(),
);
}
void jumpTabAndPage(int index) {
controller.pageIndex = index; // 更新页面索引
controller.updateUi(); // 更新 UI
controller.pageIndex = index;
controller.updateUi();
_pageController.jumpToPage(controller.pageIndex);
}
// 对应的页面
List<Widget> _buildPages() {
return [
MapPage(),
ReservationPage(),
CarInfoPage(),
MinePage(),
];
return [ReservationPage(), MapPage(), MallPage(), CarInfoPage(), MinePage()];
}
//导航栏
// 自定义导航栏 (悬浮胶囊样式)
Widget _buildNavigationBar() {
return NavigationX(
currentIndex: controller.pageIndex, // 当前选中的tab索引
onTap: (index) {
jumpTabAndPage(index);
}, // 切换tab事件
items: [
NavigationItemModel(
label: '地图',
icon: AntdIcon.location,
selectedIcon: AntdIcon.location_fill,
dot: false,
return SafeArea(
child: Container(
height: 50.h,
margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
decoration: BoxDecoration(
color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
NavigationItemModel(
label: '加氢预约',
icon: AntdIcon.orderedlist,
selectedIcon: AntdIcon.calendar_fill,
badge: '99+',
dot: true,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildNavItem(0, "ic_h2_select@2x", "ic_h2@2x"),
_buildNavItem(1, "ic_map_select@2x", "ic_map@2x"),
_buildNavItem(2, "ic_mall_select@2x", "ic_mall@2x"),
_buildNavItem(3, "ic_car_select@2x", "ic_car@2x"),
_buildNavItem(4, "ic_user_select@2x", "ic_user@2x"),
],
),
NavigationItemModel(
label: '车辆信息',
icon: AntdIcon.car,
selectedIcon: AntdIcon.car_fill,
),
);
}
// 构建单个导航项
Widget _buildNavItem(int index, String icon, String selectedIcon) {
bool isSelected = controller.pageIndex == index;
return GestureDetector(
onTap: () => jumpTabAndPage(index),
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF006633) : Colors.transparent, // 选中时的深绿色背景
borderRadius: BorderRadius.circular(20),
),
NavigationItemModel(
label: '我的',
icon: AntdIcon.user,
selectedIcon: AntdIcon.user,
child: SizedBox(
height: 24,
width: 24,
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),
),
],
),
);
}
@@ -81,10 +97,10 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
id: 'baseWidgets',
builder: (_) {
return Scaffold(
extendBody: false,
extendBody: true, // 重要:让 body 延伸到导航栏后面
resizeToAvoidBottomInset: false,
bottomNavigationBar: _buildNavigationBar(),
body: SafeArea(child: _buildView()),
body: _buildView(), // 移除 SafeArea 以获得更好的全屏沉浸感
);
},
);

View File

@@ -48,9 +48,9 @@ class AttachmentViewerController extends GetxController {
}
},
);
localFilePath.value = savePath;
} catch (e) {
showErrorToast('PDF文件加载失败请检查网络或文件链接');
print('PDF Download Error: $e');

View File

@@ -12,12 +12,12 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
@override
Widget build(BuildContext context) {
Get.put(AttachmentViewerController());
final fileName = controller.url.split('/').last;
// final fileName = controller.url.split('/').last;
return Scaffold(
appBar: AppBar(
title: Text(
fileName,
"证件详情",
style: const TextStyle(fontSize: 16),
overflow: TextOverflow.ellipsis,
),
@@ -40,11 +40,10 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
if (controller.fileType == 'pdf') {
if (controller.localFilePath.isNotEmpty) {
return PDFView(
key: ValueKey(controller.localFilePath.value),
filePath: controller.localFilePath.value,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
swipeHorizontal: true,
autoSpacing: true,
pageFling: true,
onRender: (pages) {
print("PDF rendered with $pages pages.");

View File

@@ -1,36 +1,87 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:path_provider/path_provider.dart';
import 'attachment_viewer_page.dart';
class CertificateViewerController extends GetxController {
class CertificateViewerController extends GetxController with BaseControllerMixin {
late final String title;
late final List<String> attachments;
// --- 新增: 状态管理 ---
/// 用于存储网络PDF的本地路径key是网络urlvalue是本地路径
final RxMap<String, String> localPdfPaths = <String, String>{}.obs;
/// 用于跟踪每个附件的加载状态key是网络url
final RxMap<String, bool> isLoading = <String, bool>{}.obs;
@override
String get builderId => "certificateviewer";
@override
void onInit() {
super.onInit();
// 从 Get.to 的 arguments 中获取标题和附件列表
title = Get.arguments['title'] ?? '证件详情';
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) {
if (url.isEmpty) {
showErrorToast('附件链接无效');
return;
}
Get.to(
() => const AttachmentViewerPage(),
arguments: {
'url': url,
},
);
Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
}
/// 检查 URL 是否为 PDF,以便在视图中显示不同的图标
/// 检查 URL 是否为 PDF (此方法保持不变)
bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf');
}

View File

@@ -1,51 +1,114 @@
import 'package:flutter/material.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart'; // 引入PDFView
import 'package:get/get.dart';
import 'package:getx_scaffold/common/common.dart';
import 'certificate_viewer_controller.dart';
class CertificateViewerPage extends GetView<CertificateViewerController> {
const CertificateViewerPage({Key? key}) : super(key: key);
const CertificateViewerPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(CertificateViewerController());
return Scaffold(
appBar: AppBar(
title: Text(controller.title),
centerTitle: true,
),
body: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
itemCount: controller.attachments.length,
itemBuilder: (context, index) {
final url = controller.attachments[index];
// 从 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,
return GetBuilder<CertificateViewerController>(
init: CertificateViewerController(),
id: 'certificateviewer',
builder: (_) {
return Scaffold(
appBar: AppBar(title: Text(controller.title)),
body: Column(
children: [
SizedBox(height: 16,),
Text(
"点击可查看大图",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
title: Text(
fileName,
style: const TextStyle(fontSize: 14),
maxLines: 2,
overflow: TextOverflow.ellipsis,
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: controller.attachments.length,
itemBuilder: (context, index) {
final url = controller.attachments[index];
return _buildAttachmentItem(url);
},
),
),
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)),
);
}
}

View File

@@ -2,9 +2,12 @@ import 'package:get/get.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/vehicle_info.dart';
import 'package:ln_jq_app/pages/c_page/car_info/attachment_viewer_page.dart';
import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import 'package:path_provider/path_provider.dart';
import 'certificate_viewer_page.dart';
import 'dart:io';
class CarInfoController extends GetxController with BaseControllerMixin {
@override
@@ -12,20 +15,78 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// --- 车辆基本信息 ---
String plateNumber = "";
String vin = "未知";
String modelName = "未知";
String brandName = "未知";
String vin = "-";
String modelName = "-";
String brandName = "-";
// --- 证件附件列表 ---
final RxList<String> drivingAttachments = <String>[].obs;
final RxList<String> operationAttachments = <String>[].obs;
final RxList<String> hydrogenationAttachments = <String>[].obs;
final RxList<String> registerAttachments = <String>[].obs;
String color = "";
String hydrogenCapacity = "";
String rentFromCompany = "";
String address = "";
bool isNotice = false;
@override
void onInit() {
super.onInit();
getUserBindCarInfo();
msgNotice();
}
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
@override
void onReady() {
super.onReady();
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
if (!StorageService.to.hasShownBindVehicleDialog &&
StorageService.to.isLoggedIn &&
StorageService.to.loginChannel == LoginChannel.driver &&
!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 {
@@ -43,7 +104,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
final response = await HttpService.to.get(
'appointment/vehicle/getPicInfoByVin?vin=$vin',
);
if (response != null && response.data != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
@@ -60,8 +121,25 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// 将解析出的 URL 列表赋值给对应的 RxList
drivingAttachments.assignAll(parseAttachments(data['drivingAttachment']));
operationAttachments.assignAll(parseAttachments(data['operationAttachment']));
hydrogenationAttachments.assignAll(parseAttachments(data['hydrogenationAttachment']));
hydrogenationAttachments.assignAll(
parseAttachments(data['hydrogenationAttachment']),
);
registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
// 初始化时开始加载所有PDF
attachments = [
...drivingAttachments,
...operationAttachments,
...hydrogenationAttachments,
...registerAttachments,
];
color = data['color'].toString();
hydrogenCapacity = data['hydrogenCapacity'].toString();
rentFromCompany = data['rentFromCompany'].toString();
address = data['address'].toString();
loadAllPdfs();
}
}
updateUi();
@@ -70,7 +148,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
/// 跳转到证件查看页面
void navigateToCertificateViewer(String title, List<String> attachments) {
if(!StorageService.to.hasVehicleInfo){
if (!StorageService.to.hasVehicleInfo) {
showToast('请先绑定车辆');
return;
}
@@ -80,10 +158,72 @@ class CarInfoController extends GetxController with BaseControllerMixin {
}
Get.to(
() => const CertificateViewerPage(),
arguments: {
'title': title,
'attachments': attachments,
},
arguments: {'title': title, 'attachments': attachments},
);
}
/// 导航到通用的附件查看器页面
void openAttachment(String url) {
if (url.isEmpty) {
showErrorToast('附件链接无效');
return;
}
Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
}
/// 检查 URL 是否为 PDF
bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf');
}
List<String> attachments = [];
// --- 新增: 状态管理 ---
/// 用于存储网络PDF的本地路径key是网络urlvalue是本地路径
final RxMap<String, String> localPdfPaths = <String, String>{}.obs;
/// 用于跟踪每个附件的加载状态key是网络url
final RxMap<String, bool> isLoading = <String, bool>{}.obs;
/// 遍历所有附件如果是PDF则进行下载
void loadAllPdfs() {
for (var url in attachments) {
if (isPdf(url)) {
_downloadPdf(url);
}
}
}
/// 下载单个PDF文件
Future<void> _downloadPdf(String url) async {
if (url.isEmpty) return;
// 开始加载
isLoading[url] = true;
try {
final dio = Dio();
final Directory tempDir = await getTemporaryDirectory();
final String savePath = '${tempDir.path}/${url.split('/').last}';
// 检查文件是否已存在,避免重复下载
if (await File(savePath).exists()) {
localPdfPaths[url] = savePath;
isLoading[url] = false;
return;
}
await dio.download(url, savePath);
// 下载成功后,更新本地路径
localPdfPaths[url] = savePath;
} catch (e) {
print('PDF download error for $url: $e');
// 出错时也可以更新状态以便UI显示错误提示
} finally {
// 结束加载
isLoading[url] = false;
}
}
}

View File

@@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import 'package:photo_view/photo_view.dart';
import '../../../common/styles/theme.dart';
import 'controller.dart';
class CarInfoPage extends GetView<CarInfoController> {
@@ -16,22 +20,26 @@ class CarInfoPage extends GetView<CarInfoController> {
id: 'car_info',
builder: (_) {
return Scaffold(
backgroundColor: Colors.grey[100],
backgroundColor: const Color.fromRGBO(240, 244, 247, 0.4),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildDriverInfoCard(),
const SizedBox(height: 5),
_buildCarBindingCard(),
const SizedBox(height: 5),
_buildCertificatesCard(),
const SizedBox(height: 5),
_buildTipsCard(),
],
),
child: Column(
children: [
_buildUserInfoCard(),
Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 16),
_buildCarInfoCard(),
_buildCertificatesCard(context),
const SizedBox(height: 12),
_buildSafetyReminderCard(),
SizedBox(height: 95.h),
],
),
),
],
),
),
);
@@ -39,74 +47,142 @@ class CarInfoPage extends GetView<CarInfoController> {
);
}
/// 构建顶部的司机信息卡片
Widget _buildDriverInfoCard() {
Widget _buildUserInfoCard() {
return Card(
elevation: 2,
elevation: 1,
color: Colors.white,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 50),
child: Row(
children: [
const CircleAvatar(
radius: 20,
backgroundColor: Colors.blue,
child: Icon(Icons.person, color: Colors.white, size: 34),
Stack(
children: [
CircleAvatar(
radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
),
Positioned(
right: 0,
bottom: 0,
child: SizedBox(
height: 16.h,
width: 16.w,
child: LoginUtil.getAssImg('ic_logo@2x'),
),
),
],
),
const SizedBox(width: 12),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${StorageService.to.name}",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
Row(
children: [
Text(
"${StorageService.to.name}",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8.w),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(236, 255, 234, 1),
border: Border.all(color: const Color(0xFFB7E19F)),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)),
SizedBox(width: 4),
Text(
"绿色先锋",
style: TextStyle(
color: Color(0xFF52C41A),
fontSize: 10,
),
),
],
),
),
],
),
const SizedBox(height: 4),
Text(
"${StorageService.to.phone}",
style: TextStyle(color: Colors.grey, fontSize: 11),
"羚牛ID${StorageService.to.phone}",
style: const TextStyle(color: Colors.grey, fontSize: 11),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue, width: 0.5),
IconButton(
onPressed: () async {
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
child: const Row(
children: [
Icon(Icons.shield_outlined, color: Colors.blue, size: 14),
SizedBox(width: 4),
Text(
'已认证',
style: TextStyle(
color: Colors.blue,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
],
),
),
const Divider(height: 1, indent: 16, endIndent: 16),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildStatItem('156', '服务天数'),
_buildStatItem('4.9', '评分'),
_buildStatItem('98%', '准时率'),
_buildModernStatItem(
'本月里程数',
'Accumulated',
StorageService.to.hasVehicleInfo ? '2,852km' : '-',
'',
),
const SizedBox(width: 8),
_buildModernStatItem(
'总里程',
'Refuel Count',
StorageService.to.hasVehicleInfo ? "2.5W km" : '-',
'',
),
const SizedBox(width: 8),
_buildModernStatItem(
'服务评分',
'Driver rating',
StorageService.to.hasVehicleInfo ? "4.9分" : '-',
'',
),
],
),
),
@@ -115,212 +191,383 @@ class CarInfoPage extends GetView<CarInfoController> {
);
}
// 司机信息卡片中的小统计项
Widget _buildStatItem(String value, String label) {
return Column(
children: [
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
Widget _buildModernStatItem(String title, String subtitle, String value, String unit) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(12),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
],
);
}
/// 构建车辆绑定信息卡片
Widget _buildCarBindingCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
children: [
_buildInfoRow('车牌号: ${controller.plateNumber}', '扫码绑定'),
const SizedBox(height: 12),
_buildInfoRow('车架号:', '${controller.vin}'),
const SizedBox(height: 12),
_buildInfoRow('车辆型号:', '${controller.modelName}'),
const SizedBox(height: 12),
_buildInfoRow('车辆品牌:', '${controller.brandName}'),
],
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(width: 8),
Icon(Icons.propane_rounded, size: 50, color: Colors.blue.withOpacity(0.5)),
Text(subtitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.black54)),
],
),
],
),
),
);
}
// 车辆绑定卡片中的信息行
Widget _buildInfoRow(String label, String value) {
bool isButton = value == '扫码绑定';
Widget _buildCarInfoCard() {
return Card(
elevation: 2,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
_buildDetailRow('车牌号', controller.plateNumber, isPlate: true),
const SizedBox(height: 11),
_buildDetailRow('车架号', controller.vin),
const SizedBox(height: 11),
_buildDetailRow('车辆型号', controller.modelName),
const SizedBox(height: 11),
_buildDetailRow('车辆品牌', controller.brandName),
const SizedBox(height: 10),
_buildH2LevelProgress(),
],
),
),
);
}
Widget _buildDetailRow(String label, String value, {bool isPlate = false}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontSize: 13)),
const SizedBox(width: 8),
isButton
? GestureDetector(
onTap: () async {
//判断是否绑定成功
var scanResult = await Get.to(() => const QrCodePage());
if (scanResult == true) {
controller.getUserBindCarInfo();
refreshAppui();
}
},
Text(label, style: const TextStyle(fontSize: 13, color: Colors.grey)),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isPlate)
GestureDetector(
onTap: () => controller.doQrCode(),
child: Container(
margin: EdgeInsetsGeometry.only(left: 10.w),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
border: Border.all(color: Colors.blue.shade300, width: 1),
borderRadius: BorderRadius.circular(5),
color: Colors.blue.withOpacity(0.05),
border: Border.all(color: const Color.fromRGBO(71, 174, 208, 1)),
borderRadius: BorderRadius.circular(12),
color: const Color.fromRGBO(235, 250, 255, 1),
),
child: Row(
mainAxisSize: MainAxisSize.min, // Keep the row compact
children: [
Icon(
StorageService.to.hasVehicleInfo ? Icons.repeat : Icons.search,
size: 13,
color: Colors.blue,
const Icon(
Icons.sync,
size: 12,
color: Color.fromRGBO(71, 174, 208, 1),
),
const SizedBox(width: 3),
const SizedBox(width: 4),
Text(
StorageService.to.hasVehicleInfo ? "换车牌" : value,
StorageService.to.hasVehicleInfo ? "换车牌" : "扫码绑定",
style: const TextStyle(
color: Colors.blue,
fontSize: 11,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(71, 174, 208, 1),
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
),
)
: Text(
value,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
],
);
}
/// 3. 构建车辆证件卡片
Widget _buildCertificatesCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
children: [
_buildCertificateRow(
icon: Icons.credit_card_rounded,
title: '行驶证',
attachments: controller.drivingAttachments,
),
const Divider(),
_buildCertificateRow(
icon: Icons.article_rounded,
title: '营运证',
attachments: controller.operationAttachments,
),
const Divider(),
_buildCertificateRow(
icon: Icons.propane_tank_rounded,
title: '加氢证',
attachments: controller.hydrogenationAttachments,
),
const Divider(),
_buildCertificateRow(
icon: Icons.app_registration_rounded,
title: '登记证',
attachments: controller.registerAttachments,
),
],
),
),
);
}
/// 证件展示
Widget _buildCertificateRow({
required IconData icon,
required String title,
required List<String> attachments,
}) {
return ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
radius: 24,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 28),
),
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
// 使用 Obx 响应式地显示附件数量
subtitle: Obx(
() => Text(
'${attachments.length} 个附件',
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
),
trailing: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.find_in_page_outlined, color: AppTheme.themeColor),
),
// 更新 onTap 逻辑
onTap: () => controller.navigateToCertificateViewer(title, attachments),
);
}
Widget _buildTipsCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_buildTipItem(Icons.info_outline, '请确保车辆证件齐全有效'),
const SizedBox(height: 10),
_buildTipItem(Icons.rule, '定期检查车辆状态和证件有效期'),
const SizedBox(height: 10),
_buildTipItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
],
),
),
);
}
// 提示信息卡片中的列表项
Widget _buildTipItem(IconData icon, String text) {
return Row(
Widget _buildH2LevelProgress() {
return Column(
children: [
Icon(icon, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: StorageService.to.hasVehicleInfo ? 0.75 : 0,
minHeight: 8,
backgroundColor: Color(0xFFF0F2F5),
valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(16, 185, 129, 1)),
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("H2 Level", style: TextStyle(fontSize: 11, color: Colors.grey)),
Text(
StorageService.to.hasVehicleInfo ? "75%" : "0%",
style: TextStyle(
fontSize: 11,
color: Color.fromRGBO(16, 185, 129, 1),
fontWeight: FontWeight.bold,
),
),
],
),
],
);
}
/// 3. 构建车辆证件卡片 (重构为 TabView 样式)
Widget _buildCertificatesCard(BuildContext context) {
return DefaultTabController(
length: 4,
child: Column(
children: [
TabBar(
isScrollable: false,
indicatorColor: Color.fromRGBO(16, 185, 129, 1),
labelColor: Color.fromRGBO(16, 185, 129, 1),
unselectedLabelColor: Colors.grey,
labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
indicatorSize: TabBarIndicatorSize.label,
tabs: const [
Tab(text: '行驶证'),
Tab(text: '营运证'),
Tab(text: '加氢证'),
Tab(text: '登记证'),
],
),
const SizedBox(height: 9),
SizedBox(
height: 356.h, // 给定一个高度,或者使用别的方式布局
child: TabBarView(
children: [
_buildCertificateContent('行驶证', controller.drivingAttachments),
_buildCertificateContent('营运证', controller.operationAttachments),
_buildCertificateContent('加氢资格证', controller.hydrogenationAttachments),
_buildCertificateContent('登记证', controller.registerAttachments),
],
),
),
],
),
);
}
/// 构建单个证件的展示内容
Widget _buildCertificateContent(String title, RxList<String> attachments) {
return Obx(() {
return Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: EdgeInsets.all(16.0),
child: attachments.isEmpty
? const Center(child: Text('暂无相关证件信息'))
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
//证件文字
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildCertDetailItem(
'所属公司',
controller.rentFromCompany,
isFull: false,
),
_buildCertDetailItem('运营城市', controller.address),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildCertDetailItem(
'车辆颜色',
controller.color,
valueColor: const Color(0xFF52C41A),
),
_buildCertDetailItem('氢瓶容量', controller.hydrogenCapacity),
],
),
const SizedBox(height: 16),
// 附件预览部分
GestureDetector(
onTap: () {
controller.navigateToCertificateViewer(title, attachments);
},
child: Container(
height: 184.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Color.fromRGBO(226, 232, 240, 1)),
color: Color.fromRGBO(248, 250, 252, 1),
),
child: Center(child: _buildAttachmentPreview(attachments[0])),
),
),
],
),
),
);
});
}
Widget _buildCertDetailItem(
String label,
String value, {
Color? valueColor,
bool isFull = false,
}) {
return Container(
width: isFull ? null : 140,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: valueColor ?? Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
/// 附件预览组件 (智能判断 PDF 或图片)
Widget _buildAttachmentPreview(String url) {
return AspectRatio(
aspectRatio: 4 / 3,
child: controller.isPdf(url)
? Obx(() {
final bool loading = controller.isLoading[url] ?? true;
final String? localPath = controller.localPdfPaths[url];
if (loading) {
return _buildLoadingIndicator();
} else if (localPath != null && localPath.isNotEmpty) {
return IgnorePointer(
ignoring: true, // 设置为 true 来忽略所有指针事件
child: PDFView(
backgroundColor: Color.fromRGBO(248, 250, 252, 1),
fitEachPage: true,
filePath: localPath,
fitPolicy: FitPolicy.WIDTH,
// 适配宽度
enableSwipe: false,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
preventLinkNavigation: true, // 顺便禁用PDF内部链接的跳转
),
);
} else {
// PDF加载失败
return _buildErrorIndicator();
}
})
: Image.network(
url,
fit: BoxFit.contain,
// 图片加载时显示loading
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildLoadingIndicator();
},
// 图片加载失败时显示错误
errorBuilder: (context, error, stackTrace) {
return _buildErrorIndicator();
},
),
);
}
Widget _buildLoadingIndicator() {
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
}
Widget _buildErrorIndicator() {
return const SizedBox(
height: 200,
child: Center(child: Icon(Icons.error_outline, color: Colors.red, size: 48)),
);
}
/// 安全提醒卡片
Widget _buildSafetyReminderCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(242, 249, 248, 1),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, color: AppTheme.themeColor, size: 24),
const SizedBox(width: 8),
Text(
"安全提醒",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color.fromRGBO(1, 113, 55, 1),
),
),
],
),
const SizedBox(height: 12),
const Text(
"请确保车辆证件齐全有效,定期检查车辆状态和证件有效期,以确保运输作业合规安全。",
style: TextStyle(
fontSize: 13,
color: Color.fromRGBO(1, 113, 55, 0.8),
height: 1.5,
),
),
const SizedBox(height: 8),
const Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(fontSize: 13, color: Color.fromRGBO(1, 113, 55, 0.8)),
),
],
),
);
}
}

View File

@@ -0,0 +1,113 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/c_page/mall/exchange_success/view.dart';
import '../mall_controller.dart';
class MallDetailController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'mall_detail';
late final int goodsId;
final Rx<GoodsModel?> goodsDetail = Rx<GoodsModel?>(null);
final addressController = TextEditingController();
final nameController = TextEditingController();
final phoneController = TextEditingController();
final formKey = GlobalKey<FormState>();
@override
void onInit() {
super.onInit();
goodsId = Get.arguments['goodsId'] as int;
getGoodsDetail();
}
@override
bool get listenLifecycleEvent => true;
Future<void> getGoodsDetail() async {
try {
var response = await HttpService.to.post(
'appointment/score/getScoreGoodsDetail',
data: {'goodsId': goodsId},
);
if (response != null && response.data != null) {
var result = BaseModel<GoodsModel>.fromJson(
response.data,
dataBuilder: (dataJson) => GoodsModel.fromJson(dataJson),
);
if (result.code == 0 && result.data != null) {
goodsDetail.value = result.data;
} else {
showErrorToast('加载失败: ${result.message}');
Get.back();
}
}
} catch (e) {
log('获取商品详情失败: $e');
showErrorToast('网络异常,请稍后重试');
Get.back();
} finally {
updateUi();
}
}
/// 兑换商品
void exchange() async {
if (!formKey.currentState!.validate()) {
return;
}
/*
final mallController = Get.find<MallController>();
if (mallController.userScore.value < (goodsDetail.value?.score ?? 0)) {
showWarningToast('积分不足');
return;
}*/
// 接口调用预留
showLoading('兑换中...');
final goods = goodsDetail.value;
if (goods == null) {
showErrorToast('兑换失败,请稍后重试');
return;
}
try {
var response = await HttpService.to.post(
'appointment/score/scoreExchange',
data: {
"goodsId": goods.id,
"address": addressController.text,
"name": nameController.text,
"phone": phoneController.text,
},
);
if (response != null && response.data != null) {
var result = BaseModel.fromJson(response.data);
if (result.code == 0) {
Get.off(() => MallExchangeSuccessPage());
}
}
} catch (e) {
log('兑换失败: $e');
showErrorToast('兑换失败,请稍后重试');
} finally {
dismissLoading();
}
}
@override
void onClose() {
addressController.dispose();
nameController.dispose();
phoneController.dispose();
super.onClose();
}
}

View File

@@ -0,0 +1,281 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'controller.dart';
class MallDetailPage extends GetView<MallDetailController> {
const MallDetailPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MallDetailController>(
init: MallDetailController(),
id: 'mall_detail',
builder: (_) {
return Scaffold(
backgroundColor: const Color(0xFFF7F8FA),
appBar: AppBar(
title: const Text('商品兑换'),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 20),
onPressed: () => Get.back(),
),
),
body: GestureDetector(
onTap: () {
hideKeyboard();
},
child: _buildBody(),
),
bottomNavigationBar: _buildBottomButton(),
);
},
);
}
Widget _buildBody() {
final goods = controller.goodsDetail.value;
if (goods == null) return const Center(child: Text('商品信息不存在'));
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: controller.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildGoodsInfoCard(goods),
const SizedBox(height: 24),
_buildSectionTitle('填写收货信息'),
const SizedBox(height: 16),
_buildInputLabel('详细地址'),
_buildTextField(
controller: controller.addressController,
hint: '请输入完整的收货地址',
icon: Icons.location_on_outlined,
),
const SizedBox(height: 16),
_buildInputLabel('收货人姓名'),
_buildTextField(
controller: controller.nameController,
hint: '请输入收货人姓名',
icon: Icons.person_outline,
),
const SizedBox(height: 16),
_buildInputLabel('联系电话'),
_buildTextField(
controller: controller.phoneController,
hint: '请输入手机号码',
icon: Icons.phone_android_outlined,
keyboardType: TextInputType.phone,
),
const SizedBox(height: 40),
Center(
child: Text(
'兑换成功后商品会在3个工作日内邮寄\n请注意查收',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF999999),
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
}
Widget _buildGoodsInfoCard(goods) {
return Container(
padding: const EdgeInsets.all(17),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: goods.goodsImage != null
? Image.network(
goods.goodsImage!,
width: 94.w,
height: 94.h,
fit: BoxFit.cover,
)
: Container(
width: 80,
height: 80,
color: Colors.grey[200],
child: const Icon(Icons.image, color: Colors.grey),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
goods.goodsName,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
SizedBox(height: 8.h),
Row(
children: [
Text(
'${goods.score}',
style: TextStyle(
fontSize: 20.sp,
color: Color(0xFF4CAF50),
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
Text(
'积分',
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
),
),
],
),
SizedBox(height: 10.h),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'数量: 1',
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.w500,
color: Color(0xFF666666),
),
),
),
],
),
),
],
),
);
}
Widget _buildSectionTitle(String title) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 6.w,
height: 16.h,
decoration: BoxDecoration(
color: const Color(0xFF4CAF50),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Color.fromRGBO(148, 163, 184, 1),
),
),
],
);
}
Widget _buildInputLabel(String label) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w600,
color: Color.fromRGBO(100, 116, 139, 1),
),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String hint,
required IconData icon,
TextInputType? keyboardType,
}) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
textAlign: TextAlign.start,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(
color: Color.fromRGBO(134, 144, 156, 1),
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
prefixIcon: Icon(icon, color: const Color(0xFF999999), size: 20),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '内容不能为空';
}
return null;
},
),
);
}
Widget _buildBottomButton() {
return Container(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 30),
child: ElevatedButton(
onPressed: controller.exchange,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF007A45),
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
elevation: 0,
),
child: const Text(
'兑换商品',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/common/index.dart';
import 'package:ln_jq_app/common/login_util.dart';
class MallExchangeSuccessPage extends StatelessWidget {
const MallExchangeSuccessPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 20, color: Colors.black),
onPressed: () => Get.back(), // 返回首页
),
title: const Text('商品兑换', style: TextStyle(color: Colors.black, fontSize: 18)),
),
body: Center(
child: Column(
children: [
SizedBox(height: 114.h),
_buildSuccessIcon(),
const SizedBox(height: 24),
Text(
'兑换成功',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
const SizedBox(height: 8),
Text(
'预计 3 日内发货\n请留意查收',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14.sp, color: Color(0xFF999999)),
),
const SizedBox(height: 60),
ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF007A45),
minimumSize: const Size(140, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
),
child: Text(
'返回首页',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w400,
fontSize: 16.sp,
),
),
),
],
),
),
);
}
Widget _buildSuccessIcon() {
return Container(child: LoginUtil.getAssImg("mall_pay_success@2x"));
}
}

View File

@@ -0,0 +1,160 @@
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/c_page/mall/detail/view.dart';
import 'package:ln_jq_app/pages/c_page/mall/orders/view.dart';
import 'package:ln_jq_app/pages/c_page/mall/rule/view.dart';
class GoodsModel {
final int id;
final String categoryId;
final String goodsName;
final String? goodsImage;
final int originalScore;
final int score;
final int stock;
final int status;
GoodsModel({
required this.id,
required this.categoryId,
required this.goodsName,
this.goodsImage,
required this.originalScore,
required this.score,
required this.stock,
required this.status,
});
factory GoodsModel.fromJson(Map<String, dynamic> json) {
return GoodsModel(
id: json['id'] as int,
categoryId: json['categoryId']?.toString() ?? '',
goodsName: json['goodsName']?.toString() ?? '',
goodsImage: json['goodsImage'],
originalScore: json['originalScore'] as int? ?? 0,
score: json['score'] as int? ?? 0,
stock: json['stock'] as int? ?? 0,
status: json['status'] as int? ?? 1,
);
}
}
class UserScore {
final int score;
final int todaySign;
UserScore({required this.score, required this.todaySign});
factory UserScore.fromJson(Map<String, dynamic> json) {
return UserScore(
score: json['score'] as int? ?? 0,
todaySign: json['todaySign'] as int? ?? 1,
);
}
}
class MallController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'mall';
final RxInt userScore = 0.obs;
final RxInt todaySign = 1.obs; // 0可签到1已签到
final RxList<GoodsModel> goodsList = <GoodsModel>[].obs;
final RxBool isLoading = true.obs;
@override
void onInit() {
super.onInit();
refreshData();
}
Future<void> refreshData() async {
isLoading.value = true;
await Future.wait([getUserScore(), getGoodsList()]);
isLoading.value = false;
updateUi();
}
/// 获取用户积分和签到状态
Future<void> getUserScore() async {
try {
var response = await HttpService.to.post('appointment/score/getUserScore');
if (response != null && response.data != null) {
var result = BaseModel<UserScore>.fromJson(
response.data,
dataBuilder: (dataJson) => UserScore.fromJson(dataJson),
);
if (result.code == 0 && result.data != null) {
userScore.value = result.data!.score;
todaySign.value = result.data!.todaySign;
}
}
} catch (e) {
log('获取积分失败: $e');
}
}
/// 签到逻辑
Future<void> signAction() async {
if (todaySign.value == 1) return;
showLoading('签到中...');
try {
var response = await HttpService.to.post('appointment/score/sign');
dismissLoading();
if (response != null && response.data != null) {
var result = BaseModel.fromJson(response.data);
if (result.code == 0) {
showSuccessToast('签到成功');
getUserScore(); // 签到成功后刷新积分
} else {
showErrorToast(result.message);
}
}
} catch (e) {
dismissLoading();
showErrorToast('签到异常');
}
}
/// 获取商品列表
Future<void> getGoodsList() async {
try {
var response = await HttpService.to.post(
'appointment/score/getScoreGoodsList',
data: {'categoryId': 0},
);
if (response != null && response.data != null) {
var result = BaseModel<List<GoodsModel>>.fromJson(
response.data,
dataBuilder: (dataJson) {
var list = dataJson as List;
return list.map((e) => GoodsModel.fromJson(e)).toList();
},
);
if (result.code == 0 && result.data != null) {
goodsList.assignAll(result.data!);
}
}
} catch (e) {
log('获取商品列表失败: $e');
}
}
/// 兑换商品
void exchangeGoods(GoodsModel goods) {
Get.to(() => const MallDetailPage(), arguments: {'goodsId': goods.id})
?.then((_) => refreshData());
}
///规则说明
void toRuleDes() {
Get.to(() => const MallRulePage());
}
///历史订单
void toOrders() {
Get.to(() => const MallOrdersPage());
}
}

View File

@@ -0,0 +1,345 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'mall_controller.dart';
class MallPage extends GetView<MallController> {
const MallPage({super.key});
@override
Widget build(BuildContext context) {
return GetX<MallController>(
init: MallController(),
builder: (_) {
return Scaffold(
backgroundColor: Color.fromRGBO(247, 249, 251, 1),
body: RefreshIndicator(
onRefresh: controller.refreshData,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column(children: [_buildAppBar(), _buildScoreCard()]),
),
),
_buildSectionHeader(),
_buildGoodsGrid(),
SliverToBoxAdapter(child: SizedBox(height: 120.h)),
],
),
),
);
},
);
}
Widget _buildAppBar() {
return Container(
padding: EdgeInsets.only(top: MediaQuery.of(Get.context!).padding.top, bottom: 0),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFF4CAF50),
borderRadius: BorderRadius.circular(8),
),
child: LoginUtil.getAssImg("mall_bar@2x"),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'氢能商城',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
Row(
children: [
const Text(
'Hydrogen Energy Mall',
style: TextStyle(fontSize: 12, color: Color(0xFF999999)),
),
],
),
],
),
),
/*IconButton(
onPressed: () {},
icon: const Icon(Icons.notifications_none, color: Color(0xFF333333)),
),*/
],
),
);
}
Widget _buildScoreCard() {
return Container(
margin: EdgeInsets.only(top: 22.h),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color.fromRGBO(2, 27, 31, 1), Color.fromRGBO(11, 67, 67, 1)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'我的可用积分',
style: TextStyle(color: Colors.white70, fontSize: 14.sp),
),
const SizedBox(width: 4),
GestureDetector(
onTap: (){
controller.toRuleDes();
},
child: const Icon(Icons.help_outline, color: Colors.white70, size: 14),
)
],
),
Text(
'Available points',
style: TextStyle(color: Colors.white38, fontSize: 11.sp),
),
],
),
GestureDetector(
onTap: controller.signAction,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: controller.todaySign.value == 0
? Color.fromRGBO(78, 184, 49, 1)
: Colors.grey,
borderRadius: BorderRadius.circular(10),
),
child: Text(
controller.todaySign.value == 0 ? '立即签到' : '已签到',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
controller.userScore.value.toString(),
style: TextStyle(
color: Color.fromRGBO(236, 236, 236, 1),
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
),
TextButton(
onPressed: () {
controller.toOrders();
},
child: Text(
'历史订单',
style: TextStyle(
color: Color.fromRGBO(148, 163, 184, 1),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
);
}
Widget _buildSectionHeader() {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
Container(
width: 4.w,
height: 16.h,
decoration: BoxDecoration(
color: const Color(0xFF4CAF50),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
'热门商品',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Color.fromRGBO(78, 89, 105, 1),
),
),
],
),
),
);
}
Widget _buildGoodsGrid() {
if (controller.goodsList.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.only(top: 50),
child: Text('暂无商品', style: TextStyle(color: Color(0xFF999999))),
),
),
);
}
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 14.h,
crossAxisSpacing: 19.w,
childAspectRatio: 0.75,
),
delegate: SliverChildBuilderDelegate((context, index) {
final goods = controller.goodsList[index];
return _buildGoodsItem(goods);
}, childCount: controller.goodsList.length),
),
);
}
Widget _buildGoodsItem(GoodsModel goods) {
return Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: goods.goodsImage != null
? Image.network(
goods.goodsImage!,
fit: BoxFit.cover,
width: double.infinity,
)
: Container(
color: const Color(0xFFEEEEEE),
child: const Center(
child: Icon(Icons.image, color: Colors.grey, size: 40),
),
),
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
goods.goodsName,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'${goods.score}',
style: TextStyle(
fontSize: 16.sp,
color: Color(0xFF4CAF50),
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 4),
Text(
'积分',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.bold,
color: Color(0xFF4CAF50),
),
),
],
),
GestureDetector(
onTap: () => controller.exchangeGoods(goods),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFE8F5E9),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'兑换',
style: TextStyle(
color: Color(0xFF4CAF50),
fontWeight: FontWeight.bold,
fontSize: 13.sp,
),
),
),
),
],
),
],
),
),
],
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More