Compare commits
63 Commits
dev_map
...
ec96a96be2
| Author | SHA1 | Date | |
|---|---|---|---|
| ec96a96be2 | |||
| c6e7616be2 | |||
| eae654c47e | |||
| f01875abb9 | |||
| 881da166d1 | |||
| 14b2e6b35c | |||
| 20c39a4a12 | |||
| 23fc0da5c8 | |||
| 7efd933416 | |||
| 2b33dac384 | |||
| ef60b8ed62 | |||
| bec6952707 | |||
| f68349a208 | |||
| 9242dcaf70 | |||
| 95fdfe6269 | |||
| ee88b3ada9 | |||
| ba27799c41 | |||
| c527732532 | |||
| cd223fa9bf | |||
| fdd6653fce | |||
| a497bc6469 | |||
| 3f282d15c1 | |||
| 03b35f660c | |||
| 9b6f93ca95 | |||
| eb41ecaec4 | |||
| 384de27f2c | |||
| 84b174c4a5 | |||
| 02e1946319 | |||
| d5083c1939 | |||
| fe44848529 | |||
| 572c416827 | |||
| 8df16f0787 | |||
| ce6bd3edd2 | |||
| 6997b4ac9e | |||
| a26d2478f3 | |||
| 0dded3b928 | |||
| b7caf58adf | |||
| 0df1902df2 | |||
| a8314d8a7a | |||
| 39cae906e9 | |||
| 14fd6c75d0 | |||
| 1724852a39 | |||
| a05d4ebb9b | |||
| 600cea4379 | |||
| 3dfc1dfc2c | |||
| 909dc95771 | |||
| cf0896453b | |||
| dce9718320 | |||
| 4491aa9b91 | |||
| 5364612a6f | |||
| 10867178fa | |||
| a5e2a89e4f | |||
| 26c5f9d67a | |||
| 9cd87b0535 | |||
| 45e45d8160 | |||
| 87e890f97e | |||
| dcf925b8c1 | |||
| c45863eda6 | |||
| 756bf53cf5 | |||
| f68c2d0938 | |||
| 211d0225e4 | |||
| 7d9b4d99e8 | |||
| 3dd583a278 |
7
ln_jq_app/android/.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(./gradlew assembleDebug --stacktrace)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,8 +37,8 @@ android {
|
|||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = 6
|
versionCode = 8
|
||||||
versionName = "1.2.3"
|
versionName = "1.2.5"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -72,4 +72,5 @@ flutter {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.amap.api:navi-3dmap-location-search:10.0.700_3dmap10.0.700_loc6.4.5_sea9.7.2")
|
implementation("com.amap.api:navi-3dmap-location-search:10.0.700_3dmap10.0.700_loc6.4.5_sea9.7.2")
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||||
}
|
}
|
||||||
|
|||||||
27
ln_jq_app/android/app/proguard-rules.pro
vendored
@@ -59,3 +59,30 @@
|
|||||||
-dontwarn anetwork.**
|
-dontwarn anetwork.**
|
||||||
-dontwarn com.ut.**
|
-dontwarn com.ut.**
|
||||||
-dontwarn com.ta.**
|
-dontwarn com.ta.**
|
||||||
|
|
||||||
|
|
||||||
|
#3D 地图 V5.0.0之前:
|
||||||
|
-keep class com.amap.api.maps.**{*;}
|
||||||
|
-keep class com.autonavi.amap.mapcore.*{*;}
|
||||||
|
-keep class com.amap.api.trace.**{*;}
|
||||||
|
|
||||||
|
#3D 地图 V5.0.0之后:
|
||||||
|
-keep class com.amap.api.maps.**{*;}
|
||||||
|
-keep class com.autonavi.**{*;}
|
||||||
|
-keep class com.amap.api.trace.**{*;}
|
||||||
|
|
||||||
|
#定位
|
||||||
|
-keep class com.amap.api.location.**{*;}
|
||||||
|
-keep class com.amap.api.fence.**{*;}
|
||||||
|
-keep class com.autonavi.aps.amapapi.model.**{*;}
|
||||||
|
|
||||||
|
#搜索
|
||||||
|
-keep class com.amap.api.services.**{*;}
|
||||||
|
|
||||||
|
#2D地图
|
||||||
|
-keep class com.amap.api.maps2d.**{*;}
|
||||||
|
-keep class com.amap.api.mapcore2d.**{*;}
|
||||||
|
|
||||||
|
#导航
|
||||||
|
-keep class com.amap.api.navi.**{*;}
|
||||||
|
-keep class com.autonavi.**{*;}
|
||||||
@@ -1,32 +1,36 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
<uses-permission
|
||||||
|
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
<!--定位权限-->
|
<!--定位权限-->
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<!--用于申请调用A-GPS模块-->
|
<!--用于申请调用A-GPS模块-->
|
||||||
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"></uses-permission>
|
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"></uses-permission>
|
||||||
<!--如果设置了target >= 28 如果需要启动后台定位则必须声明这个权限-->
|
<!--如果设置了target >= 28 如果需要启动后台定位则必须声明这个权限-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<!--如果您的应用需要后台定位权限,且有可能运行在Android Q设备上,并且设置了target>28,必须增加这个权限声明-->
|
<!--如果您的应用需要后台定位权限,且有可能运行在Android Q设备上,并且设置了target>28,必须增加这个权限声明-->
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="小羚羚"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/logo">
|
android:icon="@mipmap/logo"
|
||||||
|
android:label="小羚羚">
|
||||||
|
|
||||||
<!-- 高德地图Key -->
|
<!-- 高德地图Key -->
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -46,14 +50,21 @@
|
|||||||
android:theme="@android:style/Theme.NoTitleBar"
|
android:theme="@android:style/Theme.NoTitleBar"
|
||||||
android:configChanges="orientation|keyboardHidden|screenSize|navigation"
|
android:configChanges="orientation|keyboardHidden|screenSize|navigation"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
|
<!--搜索目的地 Activity-->
|
||||||
|
<activity
|
||||||
|
android:name="com.lnkj.ln_jq_app.SearchDestinationActivity"
|
||||||
|
android:theme="@android:style/Theme.NoTitleBar"
|
||||||
|
android:configChanges="orientation|keyboardHidden|screenSize|navigation"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
@@ -61,11 +72,10 @@
|
|||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme" />
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
@@ -75,30 +85,13 @@
|
|||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
||||||
<!-- 请填写你自己的- appKey -->
|
<!-- 请填写你自己的- appKey -->
|
||||||
<meta-data android:name="com.alibaba.app.appkey" android:value="335642645"/>
|
<meta-data
|
||||||
|
android:name="com.alibaba.app.appkey"
|
||||||
|
android:value="335642645" />
|
||||||
<!-- 请填写你自己的appSecret -->
|
<!-- 请填写你自己的appSecret -->
|
||||||
<meta-data android:name="com.alibaba.app.appsecret" android:value="39628204345a4240b5b645b68a5896c7"/>
|
<meta-data
|
||||||
<!-- 华为通道的参数appid -->
|
android:name="com.alibaba.app.appsecret"
|
||||||
<meta-data android:name="com.huawei.hms.client.appid" android:value="" />
|
android:value="39628204345a4240b5b645b68a5896c7" />
|
||||||
|
|
||||||
<!-- 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="" />
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -123,6 +116,7 @@
|
|||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
@@ -141,8 +135,8 @@
|
|||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.lnkj.ln_jq_app;
|
package com.lnkj.ln_jq_app;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -37,6 +38,9 @@ public class MainActivity extends FlutterActivity {
|
|||||||
"NativeFirstPage",
|
"NativeFirstPage",
|
||||||
new NativeMapFactory(this)
|
new NativeMapFactory(this)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 设置 Activity 实例到地图
|
||||||
|
NativeMapFactory.setActivity(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,6 +112,27 @@ public class MainActivity extends FlutterActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
|
// 处理从搜索目的地页面返回的结果
|
||||||
|
if (requestCode == 1000 && resultCode == RESULT_OK && data != null) {
|
||||||
|
String name = data.getStringExtra(SearchDestinationActivity.EXTRA_RESULT_NAME);
|
||||||
|
String address = data.getStringExtra(SearchDestinationActivity.EXTRA_RESULT_ADDRESS);
|
||||||
|
double lat = data.getDoubleExtra(SearchDestinationActivity.EXTRA_RESULT_LAT, 0);
|
||||||
|
double lon = data.getDoubleExtra(SearchDestinationActivity.EXTRA_RESULT_LON, 0);
|
||||||
|
String district = data.getStringExtra(SearchDestinationActivity.EXTRA_RESULT_DISTRICT);
|
||||||
|
|
||||||
|
if (name != null && lat != 0 && lon != 0) {
|
||||||
|
// 获取地图实例并设置目的地
|
||||||
|
NativeMapView mapView = NativeMapFactory.getMapView();
|
||||||
|
if (mapView != null) {
|
||||||
|
mapView.setDestination(name, address, lat, lon, district);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.lnkj.ln_jq_app;
|
package com.lnkj.ln_jq_app;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import io.flutter.plugin.common.MessageCodec;
|
import io.flutter.plugin.common.MessageCodec;
|
||||||
@@ -34,4 +35,13 @@ public class NativeMapFactory extends PlatformViewFactory {
|
|||||||
public static NativeMapView getMapView() {
|
public static NativeMapView getMapView() {
|
||||||
return mapViewInstance;
|
return mapViewInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Activity 实例
|
||||||
|
*/
|
||||||
|
public static void setActivity(Activity activity) {
|
||||||
|
if (mapViewInstance != null) {
|
||||||
|
mapViewInstance.setActivity(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,34 +8,23 @@ import android.graphics.BitmapFactory;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.graphics.drawable.GradientDrawable;
|
import android.graphics.drawable.GradientDrawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.InputType;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.inputmethod.EditorInfo;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@@ -71,11 +60,7 @@ import com.amap.api.services.geocoder.GeocodeSearch;
|
|||||||
import com.amap.api.services.geocoder.RegeocodeAddress;
|
import com.amap.api.services.geocoder.RegeocodeAddress;
|
||||||
import com.amap.api.services.geocoder.RegeocodeQuery;
|
import com.amap.api.services.geocoder.RegeocodeQuery;
|
||||||
import com.amap.api.services.geocoder.RegeocodeResult;
|
import com.amap.api.services.geocoder.RegeocodeResult;
|
||||||
import com.amap.api.services.help.Inputtips;
|
|
||||||
import com.amap.api.services.help.InputtipsQuery;
|
|
||||||
import com.amap.api.services.help.Tip;
|
|
||||||
import com.amap.api.services.route.BusRouteResult;
|
import com.amap.api.services.route.BusRouteResult;
|
||||||
import com.amap.api.services.route.DrivePath;
|
|
||||||
import com.amap.api.services.route.DriveRouteResult;
|
import com.amap.api.services.route.DriveRouteResult;
|
||||||
import com.amap.api.services.route.RideRouteResult;
|
import com.amap.api.services.route.RideRouteResult;
|
||||||
import com.amap.api.services.route.RouteSearch;
|
import com.amap.api.services.route.RouteSearch;
|
||||||
@@ -103,7 +88,7 @@ import okhttp3.Response;
|
|||||||
/**
|
/**
|
||||||
* 高德地图导航
|
* 高德地图导航
|
||||||
*/
|
*/
|
||||||
public class NativeMapView implements PlatformView, LocationSource, AMapLocationListener, GeocodeSearch.OnGeocodeSearchListener, RouteSearch.OnRouteSearchListener, AMap.OnMarkerClickListener, Inputtips.InputtipsListener {
|
public class NativeMapView implements PlatformView, LocationSource, AMapLocationListener, GeocodeSearch.OnGeocodeSearchListener, RouteSearch.OnRouteSearchListener, AMap.OnMarkerClickListener {
|
||||||
|
|
||||||
private static final String TAG = "NativeMapView";
|
private static final String TAG = "NativeMapView";
|
||||||
private final FrameLayout container;
|
private final FrameLayout container;
|
||||||
@@ -119,17 +104,14 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
private final OkHttpClient httpClient = new OkHttpClient();
|
private final OkHttpClient httpClient = new OkHttpClient();
|
||||||
|
|
||||||
// UI组件
|
// UI组件
|
||||||
private EditText endInput;
|
private TextView endInput;
|
||||||
private LinearLayout searchArea; // 规划路线面板
|
private LinearLayout searchArea; // 规划路线面板
|
||||||
private LinearLayout detailPanel; // 详情面板
|
private LinearLayout detailPanel; // 详情面板
|
||||||
|
|
||||||
private View modeMenu; //模式选择
|
private View modeMenu; //模式选择
|
||||||
private TextView tvStationName, tvStationAddr, planToggleBtn;
|
private TextView tvStationName, tvStationAddr, planToggleBtn, tvBusinessHours;
|
||||||
private ListView suggestionList; // 输入提示列表
|
|
||||||
private ArrayAdapter<String> suggestionAdapter; // 提示列表适配器
|
|
||||||
private List<Tip> currentTipList; // 当前提示列表
|
|
||||||
//时间 费用 里程 路费
|
//时间 费用 里程 路费
|
||||||
private TextView tvDuration, tvDistance, tvTolls, tvTollsFuel;
|
private TextView tvDuration, tvDistance, tvTolls, tvTollsFuel, tvPerson, tvPrice, tvPhone;
|
||||||
|
|
||||||
private LatLng currentLatLng;
|
private LatLng currentLatLng;
|
||||||
private String startName = "我的位置";
|
private String startName = "我的位置";
|
||||||
@@ -139,7 +121,6 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
private LatLng endPoint;
|
private LatLng endPoint;
|
||||||
private boolean isFirstLocation = true;
|
private boolean isFirstLocation = true;
|
||||||
private boolean isUserSelectedDestination = false; // 标识用户是否手动选择了目的地
|
private boolean isUserSelectedDestination = false; // 标识用户是否手动选择了目的地
|
||||||
private boolean isProgrammaticTextChange = false; // 标识是否是程序自动设置文本
|
|
||||||
private final List<Marker> stationMarkers = new ArrayList<>();
|
private final List<Marker> stationMarkers = new ArrayList<>();
|
||||||
|
|
||||||
// 存储token和车牌号
|
// 存储token和车牌号
|
||||||
@@ -153,25 +134,27 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
private TruckRouteData truckRouteData;
|
private TruckRouteData truckRouteData;
|
||||||
//当前定位信息
|
//当前定位信息
|
||||||
private AMapLocation mLoc;
|
private AMapLocation mLoc;
|
||||||
|
private ImageButton mLocBtn;
|
||||||
|
|
||||||
|
|
||||||
public NativeMapView(Context context, int id, Object args) {
|
public NativeMapView(Context context, int id, Object args) {
|
||||||
this.mContext = context;
|
this.mContext = context;
|
||||||
mActivity = getActivityFromContext(context);
|
mActivity = getActivityFromContext(context);
|
||||||
|
|
||||||
MapsInitializer.updatePrivacyShow(context, true, true);
|
// 确保 mActivity 不为 null,如果为 null 则使用 context
|
||||||
MapsInitializer.updatePrivacyAgree(context, true);
|
Activity activity = mActivity != null ? mActivity : (context instanceof Activity ? (Activity) context : null);
|
||||||
|
|
||||||
container = new FrameLayout(context);
|
|
||||||
container.setClickable(true);
|
|
||||||
container.setFocusable(true);
|
|
||||||
container.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
|
|
||||||
|
|
||||||
|
MapsInitializer.updatePrivacyShow(activity, true, true);
|
||||||
|
MapsInitializer.updatePrivacyAgree(activity, true);
|
||||||
|
|
||||||
mapView = new MapView(context);
|
mapView = new MapView(context);
|
||||||
mapView.onCreate(null);
|
mapView.onCreate(null);
|
||||||
aMap = mapView.getMap();
|
aMap = mapView.getMap();
|
||||||
|
|
||||||
|
container = new FrameLayout(context);
|
||||||
|
container.setClickable(true);
|
||||||
|
container.setFocusable(true);
|
||||||
|
container.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
|
||||||
container.addView(mapView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
container.addView(mapView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
initLoadingView(context);
|
initLoadingView(context);
|
||||||
@@ -204,7 +187,7 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
bottomContainer = new LinearLayout(context);
|
bottomContainer = new LinearLayout(context);
|
||||||
bottomContainer.setOrientation(LinearLayout.VERTICAL);
|
bottomContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
bottomContainer.setGravity(Gravity.BOTTOM);
|
bottomContainer.setGravity(Gravity.BOTTOM);
|
||||||
|
bottomContainer.setBackgroundResource(R.drawable.rounded_top_bg);
|
||||||
// 1. 详情面板 (用于显示加氢站详细信息,初始隐藏)
|
// 1. 详情面板 (用于显示加氢站详细信息,初始隐藏)
|
||||||
detailPanel = createDetailPanel(context);
|
detailPanel = createDetailPanel(context);
|
||||||
detailPanel.setVisibility(View.GONE);
|
detailPanel.setVisibility(View.GONE);
|
||||||
@@ -219,126 +202,89 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
searchArea.setFocusable(true);
|
searchArea.setFocusable(true);
|
||||||
searchArea.setFocusableInTouchMode(true);
|
searchArea.setFocusableInTouchMode(true);
|
||||||
|
|
||||||
endInput = new EditText(context);
|
endInput = new TextView(context);
|
||||||
endInput.setHint("请输入目的地,不输入则自动匹配推荐加氢站");
|
endInput.setHint("请输入目的地,不输入则自动匹配推荐加氢站");
|
||||||
endInput.setTextSize(13);
|
endInput.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_search, 0, 0, 0);
|
||||||
|
endInput.setCompoundDrawablePadding(dp2px(8));
|
||||||
|
endInput.setTextSize(12);
|
||||||
|
endInput.setTextColor(Color.parseColor("#333333"));
|
||||||
endInput.setHintTextColor(Color.GRAY);
|
endInput.setHintTextColor(Color.GRAY);
|
||||||
endInput.setPadding(dp2px(12), dp2px(12), dp2px(12), dp2px(12));
|
endInput.setPadding(dp2px(12), dp2px(12), dp2px(12), dp2px(12));
|
||||||
endInput.setBackground(getRoundedDrawable(Color.parseColor("#F8F8F8"), 8));
|
endInput.setBackground(getRoundedDrawable(Color.parseColor("#F8F8F8"), 8));
|
||||||
endInput.setSingleLine(true);
|
endInput.setSingleLine(true);
|
||||||
endInput.setInputType(InputType.TYPE_CLASS_TEXT); // 明确输入类型
|
endInput.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
endInput.setImeOptions(EditorInfo.IME_ACTION_SEARCH);
|
endInput.setOnClickListener(v -> {
|
||||||
endInput.setFocusable(true);
|
try {
|
||||||
endInput.setFocusableInTouchMode(true);
|
Log.d(TAG, "endInput clicked, mActivity=" + (mActivity != null ? "not null" : "null"));
|
||||||
endInput.setClickable(true);
|
|
||||||
endInput.setCursorVisible(true);
|
|
||||||
|
|
||||||
// 针对 EditText 的特殊处理
|
// 设置回调
|
||||||
endInput.setOnTouchListener((v, event) -> {
|
SearchDestinationActivity.setCallback((name, address, lat, lon, district) -> {
|
||||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
Log.d(TAG, "Callback received: " + name + ", lat=" + lat + ", lon=" + lon);
|
||||||
v.requestFocus();
|
// 直接设置,不依赖 mActivity.runOnUiThread
|
||||||
v.postDelayed(() -> {
|
setDestination(name, address, lat, lon, district);
|
||||||
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
if (imm != null) {
|
|
||||||
// 使用 SHOW_FORCED 或确保结果成功
|
|
||||||
imm.showSoftInput(v, InputMethodManager.SHOW_FORCED);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理搜索键按下
|
// 跳转到搜索目的地页面 - 使用更安全的方式
|
||||||
endInput.setOnEditorActionListener((v, actionId, event) -> {
|
Intent intent = new Intent(mContext, SearchDestinationActivity.class);
|
||||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
suggestionList.setVisibility(View.GONE);
|
|
||||||
// 可以在这里直接触发路线规划,但保持原来的按钮点击逻辑
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化提示列表
|
Log.d(TAG, "Starting SearchDestinationActivity with intent");
|
||||||
currentTipList = new ArrayList<>();
|
mContext.startActivity(intent);
|
||||||
suggestionList = new ListView(context);
|
|
||||||
suggestionList.setBackgroundColor(Color.WHITE);
|
|
||||||
suggestionList.setDividerHeight(1);
|
|
||||||
suggestionList.setDivider(new ColorDrawable(Color.LTGRAY));
|
|
||||||
suggestionList.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
|
|
||||||
suggestionAdapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1);
|
|
||||||
suggestionList.setAdapter(suggestionAdapter);
|
|
||||||
suggestionList.setVisibility(View.GONE);
|
|
||||||
suggestionList.setOnItemClickListener((parent, view, position, id) -> {
|
|
||||||
Tip tip = currentTipList.get(position);
|
|
||||||
if (tip.getPoint() != null) {
|
|
||||||
endPoint = new LatLng(tip.getPoint().getLatitude(), tip.getPoint().getLongitude());
|
|
||||||
String name = tip.getName();
|
|
||||||
String district = tip.getDistrict();
|
|
||||||
endName = name;
|
|
||||||
isUserSelectedDestination = true; // 标识用户手动选择了目的地
|
|
||||||
isProgrammaticTextChange = true; // 标识是程序自动设置文本
|
|
||||||
endInput.setText(district != null && !district.isEmpty() ? name + " " + district : name);
|
|
||||||
isProgrammaticTextChange = false; // 恢复标志
|
|
||||||
suggestionList.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
endInput.addTextChangedListener(new TextWatcher() {
|
} catch (Exception e) {
|
||||||
@Override
|
Log.e(TAG, "Failed to start SearchDestinationActivity", e);
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
e.printStackTrace();
|
||||||
|
// 显示错误提示
|
||||||
|
try {
|
||||||
|
if (mContext != null) {
|
||||||
|
Toast.makeText(mContext, "无法打开搜索页面: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
} catch (Exception toastException) {
|
||||||
@Override
|
Log.e(TAG, "Failed to show toast", toastException);
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
// 如果是程序自动设置文本,不触发搜索
|
|
||||||
if (isProgrammaticTextChange) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s.length() > 1) {
|
|
||||||
// 使用当前位置的城市进行搜索
|
|
||||||
String city = currentLatLng != null ? "" : "";
|
|
||||||
InputtipsQuery query = new InputtipsQuery(s.toString(), city);
|
|
||||||
query.setCityLimit(true);
|
|
||||||
Inputtips inputtips = new Inputtips(mContext, query);
|
|
||||||
inputtips.setInputtipsListener(NativeMapView.this);
|
|
||||||
inputtips.requestInputtipsAsyn();
|
|
||||||
} else {
|
|
||||||
suggestionList.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable s) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
endInput.setOnFocusChangeListener((v, hasFocus) -> {
|
|
||||||
if (!hasFocus) {
|
|
||||||
suggestionList.setVisibility(View.GONE);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
searchArea.addView(endInput);
|
searchArea.addView(endInput);
|
||||||
searchArea.addView(suggestionList);
|
|
||||||
|
|
||||||
View vSpace = new View(context);
|
View vSpace = new View(context);
|
||||||
searchArea.addView(vSpace, new LinearLayout.LayoutParams(1, dp2px(12)));
|
searchArea.addView(vSpace, new LinearLayout.LayoutParams(1, dp2px(12)));
|
||||||
|
|
||||||
Button planBtn = new Button(context);
|
// 创建自定义按钮布局
|
||||||
planBtn.setText("规划路线");
|
LinearLayout planBtnContainer = new LinearLayout(context);
|
||||||
planBtn.setTextColor(Color.WHITE);
|
planBtnContainer.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
planBtn.setTypeface(Typeface.DEFAULT_BOLD);
|
planBtnContainer.setGravity(Gravity.CENTER);
|
||||||
planBtn.setBackground(getRoundedDrawable(Color.parseColor("#017143"), 99));
|
planBtnContainer.setBackground(getRoundedDrawable(Color.parseColor("#017143"), 99));
|
||||||
planBtn.setOnClickListener(v -> calculateRouteBeforeNavi());
|
planBtnContainer.setPadding(0, 0, 0, 0);
|
||||||
searchArea.addView(planBtn, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp2px(48)));
|
|
||||||
|
// 添加图标
|
||||||
|
ImageView pathIcon = new ImageView(context);
|
||||||
|
pathIcon.setImageResource(R.drawable.ic_path);
|
||||||
|
pathIcon.setColorFilter(Color.WHITE);
|
||||||
|
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(dp2px(20), dp2px(20));
|
||||||
|
iconParams.rightMargin = dp2px(6); // 图标和文字之间的间距
|
||||||
|
planBtnContainer.addView(pathIcon, iconParams);
|
||||||
|
|
||||||
|
// 添加文字
|
||||||
|
TextView planText = new TextView(context);
|
||||||
|
planText.setText("规划路线");
|
||||||
|
planText.setTextColor(Color.WHITE);
|
||||||
|
planText.setTextSize(14);
|
||||||
|
planText.setTypeface(Typeface.DEFAULT_BOLD);
|
||||||
|
planText.setGravity(Gravity.CENTER);
|
||||||
|
planBtnContainer.addView(planText);
|
||||||
|
|
||||||
|
// 设置点击事件
|
||||||
|
planBtnContainer.setOnClickListener(v -> calculateRouteBeforeNavi());
|
||||||
|
searchArea.addView(planBtnContainer, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp2px(40)));
|
||||||
|
|
||||||
bottomContainer.addView(searchArea);
|
bottomContainer.addView(searchArea);
|
||||||
|
|
||||||
// 设置统一的底部间距
|
// 设置统一的底部间距
|
||||||
FrameLayout.LayoutParams bottomParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
FrameLayout.LayoutParams bottomParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
bottomParams.gravity = Gravity.BOTTOM;
|
bottomParams.gravity = Gravity.BOTTOM;
|
||||||
bottomParams.setMargins(dp2px(12), 0, dp2px(12), dp2px(65));
|
|
||||||
container.addView(bottomContainer, bottomParams);
|
container.addView(bottomContainer, bottomParams);
|
||||||
|
container.setPadding(0, 0, 0, dp2px(65));
|
||||||
|
|
||||||
|
|
||||||
// --- 模式选择菜单 ---
|
// --- 模式选择菜单 ---
|
||||||
@@ -347,9 +293,10 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
// 布局参数:位于规划按钮上方
|
// 布局参数:位于规划按钮上方
|
||||||
FrameLayout.LayoutParams menuParams = new FrameLayout.LayoutParams(dp2px(130), ViewGroup.LayoutParams.WRAP_CONTENT);
|
FrameLayout.LayoutParams menuParams = new FrameLayout.LayoutParams(dp2px(130), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
menuParams.gravity = Gravity.BOTTOM | Gravity.END;
|
menuParams.gravity = Gravity.BOTTOM | Gravity.END;
|
||||||
menuParams.setMargins(0, 0, dp2px(15), dp2px(330)); // 高度根据按钮位置调整
|
menuParams.setMargins(0, 0, dp2px(15), dp2px(230)); // 高度根据按钮位置调整
|
||||||
container.addView(modeMenu, menuParams);
|
container.addView(modeMenu, menuParams);
|
||||||
|
|
||||||
|
|
||||||
// 加氢规划圆形按钮
|
// 加氢规划圆形按钮
|
||||||
planToggleBtn = new TextView(context);
|
planToggleBtn = new TextView(context);
|
||||||
planToggleBtn.setText("加氢\n规划");
|
planToggleBtn.setText("加氢\n规划");
|
||||||
@@ -370,71 +317,37 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
int layoutSize = dp2px(44);
|
int layoutSize = dp2px(44);
|
||||||
FrameLayout.LayoutParams toggleParams = new FrameLayout.LayoutParams(layoutSize, layoutSize);
|
FrameLayout.LayoutParams toggleParams = new FrameLayout.LayoutParams(layoutSize, layoutSize);
|
||||||
toggleParams.gravity = Gravity.BOTTOM | Gravity.END;
|
toggleParams.gravity = Gravity.BOTTOM | Gravity.END;
|
||||||
toggleParams.setMargins(0, 0, dp2px(12), dp2px(340)); // 位于定位按钮上方
|
toggleParams.setMargins(0, 0, dp2px(12), dp2px(210)); // 位于定位按钮上方
|
||||||
container.addView(planToggleBtn, toggleParams);
|
container.addView(planToggleBtn, toggleParams);
|
||||||
|
|
||||||
// --- 右下角定位按钮 ---
|
// --- 右下角定位按钮 ---
|
||||||
ImageButton locBtn = new ImageButton(context);
|
mLocBtn = new ImageButton(context);
|
||||||
locBtn.setImageResource(R.drawable.ic_location);
|
mLocBtn.setImageResource(R.drawable.ic_location);
|
||||||
// 设置自定义的白色圆形背景
|
// 设置自定义的白色圆形背景
|
||||||
locBtn.setBackgroundColor(Color.TRANSPARENT);
|
mLocBtn.setBackgroundColor(Color.TRANSPARENT);
|
||||||
// 设置投影(仅在 API 21+ 有效,能产生干净的阴影而非黑块)
|
// 设置投影(仅在 API 21+ 有效,能产生干净的阴影而非黑块)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
locBtn.setElevation(dp2px(4));
|
mLocBtn.setElevation(dp2px(4));
|
||||||
}
|
}
|
||||||
locBtn.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
mLocBtn.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||||
int padding = dp2px(2);
|
int padding = dp2px(2);
|
||||||
locBtn.setPadding(padding, padding, padding, padding);
|
mLocBtn.setPadding(padding, padding, padding, padding);
|
||||||
locBtn.setOnClickListener(v -> {
|
mLocBtn.setOnClickListener(v -> {
|
||||||
if (currentLatLng != null)
|
if (currentLatLng != null)
|
||||||
aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(currentLatLng, 15f));
|
aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(currentLatLng, 15f));
|
||||||
});
|
});
|
||||||
FrameLayout.LayoutParams locParams = new FrameLayout.LayoutParams(layoutSize, layoutSize);
|
FrameLayout.LayoutParams locParams = new FrameLayout.LayoutParams(layoutSize, layoutSize);
|
||||||
locParams.setMargins(0, 0, dp2px(15), dp2px(285)); // 调高一点,避开底部的面板
|
locParams.setMargins(0, 0, dp2px(15), dp2px(150)); // 调高一点,避开底部的面板
|
||||||
locParams.gravity = Gravity.BOTTOM | Gravity.END;
|
locParams.gravity = Gravity.BOTTOM | Gravity.END;
|
||||||
container.addView(locBtn, locParams);
|
container.addView(mLocBtn, locParams);
|
||||||
|
|
||||||
//最后调用监听函数
|
|
||||||
addKeyboardListener();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addKeyboardListener() {
|
|
||||||
container.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
|
||||||
Rect r = new Rect();
|
|
||||||
// 获取当前窗口可视区域
|
|
||||||
container.getWindowVisibleDisplayFrame(r);
|
|
||||||
|
|
||||||
// screenHeight - 可视区域高度 = 键盘高度
|
|
||||||
int screenHeight = container.getRootView().getHeight();
|
|
||||||
int keypadHeight = screenHeight - r.bottom;
|
|
||||||
|
|
||||||
// 如果键盘高度大于屏幕的 15%,说明键盘弹出了
|
|
||||||
if (keypadHeight > screenHeight * 0.15) {
|
|
||||||
// 键盘弹出:增加底部 Margin
|
|
||||||
updateBottomMargin(keypadHeight);
|
|
||||||
} else {
|
|
||||||
// 键盘收起:恢复原有 Margin (你代码中设置的是 dp2px(65))
|
|
||||||
updateBottomMargin(dp2px(65));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateBottomMargin(int bottomPx) {
|
|
||||||
if (bottomContainer != null) {
|
|
||||||
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) bottomContainer.getLayoutParams();
|
|
||||||
if (params != null) {
|
|
||||||
// 动态设置底部间距
|
|
||||||
params.setMargins(dp2px(12), 0, dp2px(12), bottomPx);
|
|
||||||
bottomContainer.setLayoutParams(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private LinearLayout createDetailPanel(Context context) {
|
private LinearLayout createDetailPanel(Context context) {
|
||||||
LinearLayout panel = new LinearLayout(context);
|
LinearLayout panel = new LinearLayout(context);
|
||||||
panel.setOrientation(LinearLayout.VERTICAL);
|
panel.setOrientation(LinearLayout.VERTICAL);
|
||||||
panel.setBackground(getRoundedDrawable(Color.WHITE, 16));
|
int padding = dp2px(15);
|
||||||
panel.setPadding(dp2px(20), dp2px(20), dp2px(20), dp2px(20));
|
panel.setPadding(padding, padding, padding, padding);
|
||||||
|
|
||||||
// --- (包含标题和关闭按钮) ---
|
// --- (包含标题和关闭按钮) ---
|
||||||
LinearLayout titleLayout = new LinearLayout(context);
|
LinearLayout titleLayout = new LinearLayout(context);
|
||||||
@@ -455,10 +368,7 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
ivClose.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
|
ivClose.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
|
||||||
ivClose.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
|
ivClose.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
|
||||||
ivClose.setOnClickListener(v -> {
|
ivClose.setOnClickListener(v -> {
|
||||||
if (detailPanel != null)
|
resetView();
|
||||||
detailPanel.setVisibility(View.GONE);
|
|
||||||
searchArea.setVisibility(View.VISIBLE);
|
|
||||||
planToggleBtn.setVisibility(View.VISIBLE);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
LinearLayout.LayoutParams closeParams = new LinearLayout.LayoutParams(dp2px(28), dp2px(28));
|
LinearLayout.LayoutParams closeParams = new LinearLayout.LayoutParams(dp2px(28), dp2px(28));
|
||||||
@@ -466,6 +376,13 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
|
|
||||||
panel.addView(titleLayout);
|
panel.addView(titleLayout);
|
||||||
|
|
||||||
|
//营业时间和地址
|
||||||
|
tvBusinessHours = new TextView(context);
|
||||||
|
tvBusinessHours.setTextSize(13);
|
||||||
|
tvBusinessHours.setPadding(0, dp2px(4), 0, 0);
|
||||||
|
tvBusinessHours.setTextColor(Color.GRAY);
|
||||||
|
panel.addView(tvBusinessHours);
|
||||||
|
|
||||||
tvStationAddr = new TextView(context);
|
tvStationAddr = new TextView(context);
|
||||||
tvStationAddr.setTextSize(13);
|
tvStationAddr.setTextSize(13);
|
||||||
tvStationAddr.setPadding(0, dp2px(4), 0, 0);
|
tvStationAddr.setPadding(0, dp2px(4), 0, 0);
|
||||||
@@ -482,8 +399,8 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
row1.setOrientation(LinearLayout.HORIZONTAL);
|
row1.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
row1.setGravity(Gravity.CENTER_VERTICAL);
|
row1.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
row1.setPadding(0, dp2px(8), 0, 0); // 增加行间距
|
row1.setPadding(0, dp2px(8), 0, 0); // 增加行间距
|
||||||
tvDuration = createInfoItem(row1, R.drawable.ic_time, "预计时间:", "", 1.0f);
|
tvDuration = createInfoItem(row1, R.drawable.ic_time, "预计时间:", "-", 1.0f);
|
||||||
tvTollsFuel = createInfoItem(row1, R.drawable.ic_fuel, "加氢费用:", "", 1.0f);
|
tvTollsFuel = createInfoItem(row1, R.drawable.ic_fuel, "加氢费用:", "-", 1.0f);
|
||||||
routeInfoLayout.addView(row1);
|
routeInfoLayout.addView(row1);
|
||||||
|
|
||||||
// 第二行:里程 + 过路费
|
// 第二行:里程 + 过路费
|
||||||
@@ -493,17 +410,34 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
row2.setPadding(0, dp2px(10), 0, 0); // 增加行间距
|
row2.setPadding(0, dp2px(10), 0, 0); // 增加行间距
|
||||||
|
|
||||||
// 里程
|
// 里程
|
||||||
tvDistance = createInfoItem(row2, R.drawable.ic_mileage, "行驶里程:", "", 1.0f);
|
tvDistance = createInfoItem(row2, R.drawable.ic_mileage, "行驶里程:", "-", 1.0f);
|
||||||
// 过路费
|
// 过路费
|
||||||
tvTolls = createInfoItem(row2, R.drawable.ic_toll, "过路费:", "", 1.0f);
|
tvTolls = createInfoItem(row2, R.drawable.ic_toll, "过路费:", "-", 1.0f);
|
||||||
|
|
||||||
routeInfoLayout.addView(row2);
|
routeInfoLayout.addView(row2);
|
||||||
|
|
||||||
|
//第三行
|
||||||
|
LinearLayout row3 = new LinearLayout(context);
|
||||||
|
row3.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row3.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
row3.setPadding(0, dp2px(8), 0, 0); // 增加行间距
|
||||||
|
tvPerson = createInfoItem(row3, R.drawable.ic_person, "站联系人:", "-", 1.0f);
|
||||||
|
tvPrice = createInfoItem(row3, R.drawable.ic_price, "加氢价格:", "-", 1.0f);
|
||||||
|
routeInfoLayout.addView(row3);
|
||||||
|
|
||||||
|
LinearLayout row4 = new LinearLayout(context);
|
||||||
|
row4.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row4.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
row4.setPadding(0, dp2px(8), 0, 0); // 增加行间距
|
||||||
|
tvPhone = createInfoItem(row4, R.drawable.ic_phone, "联系方式:", "-", 1.0f);
|
||||||
|
routeInfoLayout.addView(row4);
|
||||||
|
|
||||||
|
|
||||||
panel.addView(routeInfoLayout);
|
panel.addView(routeInfoLayout);
|
||||||
|
|
||||||
Button startNaviBtn = new Button(context);
|
Button startNaviBtn = new Button(context);
|
||||||
startNaviBtn.setText("开始导航");
|
startNaviBtn.setText("开始导航");
|
||||||
startNaviBtn.setTextColor(Color.WHITE);
|
startNaviBtn.setTextColor(Color.WHITE);
|
||||||
startNaviBtn.setBackground(getRoundedDrawable(Color.parseColor("#017143"), 10));
|
startNaviBtn.setBackground(getRoundedDrawable(Color.parseColor("#017143"), 99));
|
||||||
startNaviBtn.setOnClickListener(v -> startRouteSearch());
|
startNaviBtn.setOnClickListener(v -> startRouteSearch());
|
||||||
|
|
||||||
LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp2px(48));
|
LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp2px(48));
|
||||||
@@ -513,6 +447,17 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void resetView() {
|
||||||
|
if (detailPanel != null)
|
||||||
|
detailPanel.setVisibility(View.GONE);
|
||||||
|
searchArea.setVisibility(View.VISIBLE);
|
||||||
|
planToggleBtn.setVisibility(View.VISIBLE);
|
||||||
|
mLocBtn.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGetInputtips = false;
|
||||||
|
|
||||||
private void calculateRouteBeforeNavi() {
|
private void calculateRouteBeforeNavi() {
|
||||||
if (startPoint == null) {
|
if (startPoint == null) {
|
||||||
Toast.makeText(mContext, "正在定位...", Toast.LENGTH_SHORT).show();
|
Toast.makeText(mContext, "正在定位...", Toast.LENGTH_SHORT).show();
|
||||||
@@ -524,80 +469,124 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//如果是输入地址提示内容
|
||||||
|
|
||||||
|
if (isGetInputtips) {
|
||||||
|
truckRouteData = null;
|
||||||
|
|
||||||
|
RouteSearch.FromAndTo fromAndTo = new RouteSearch.FromAndTo(new LatLonPoint(startPoint.latitude, startPoint.longitude), new LatLonPoint(endPoint.latitude, endPoint.longitude));
|
||||||
|
RouteSearch.DriveRouteQuery query = new RouteSearch.DriveRouteQuery(fromAndTo, RouteSearch.DRIVING_SINGLE_DEFAULT, null, null, "");
|
||||||
|
routeSearch.calculateDriveRouteAsyn(query);
|
||||||
|
|
||||||
|
|
||||||
|
// 开始规划前隐藏输入框面板
|
||||||
|
searchArea.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
fetchTruckRouteAlgorithm(mLoc);
|
fetchTruckRouteAlgorithm(mLoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onGetInputtips(List<Tip> tipList, int rCode) {
|
|
||||||
if (rCode == 1000 && tipList != null && !tipList.isEmpty()) {
|
|
||||||
currentTipList.clear();
|
|
||||||
currentTipList.addAll(tipList);
|
|
||||||
|
|
||||||
List<String> suggestionNames = new ArrayList<>();
|
|
||||||
for (Tip tip : tipList) {
|
|
||||||
String name = tip.getName();
|
|
||||||
String district = tip.getDistrict();
|
|
||||||
// 在提示中显示名称和区域
|
|
||||||
String displayText = district != null && !district.isEmpty() ? name + " " + district : name;
|
|
||||||
suggestionNames.add(displayText);
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestionAdapter.clear();
|
|
||||||
suggestionAdapter.addAll(suggestionNames);
|
|
||||||
suggestionAdapter.notifyDataSetChanged();
|
|
||||||
|
|
||||||
// 显示提示列表
|
|
||||||
new Handler(Looper.getMainLooper()).post(() -> {
|
|
||||||
if (suggestionNames.size() > 0) {
|
|
||||||
suggestionList.setVisibility(View.VISIBLE);
|
|
||||||
// 限制显示的高度
|
|
||||||
int height = Math.min(suggestionNames.size() * dp2px(45), dp2px(200));
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
height
|
|
||||||
);
|
|
||||||
suggestionList.setLayoutParams(params);
|
|
||||||
} else {
|
|
||||||
suggestionList.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 请求失败或无结果,隐藏提示列表
|
|
||||||
new Handler(Looper.getMainLooper()).post(() -> suggestionList.setVisibility(View.GONE));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDriveRouteSearched(DriveRouteResult result, int rCode) {
|
public void onDriveRouteSearched(DriveRouteResult result, int rCode) {
|
||||||
|
if (mActivity == null) {
|
||||||
|
Log.e(TAG, "mActivity is null in onDriveRouteSearched");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mActivity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
String hydrogenCost = "-"; // 默认显示横线
|
||||||
|
String hydrogenPrice = "-"; // 默认显示横线
|
||||||
|
String liaisonName = "-";
|
||||||
|
String liaisonPhone = "-";
|
||||||
|
String startBusiness = "-";
|
||||||
|
String endBusiness = "-";
|
||||||
|
double distanceKm = 0;
|
||||||
|
String distanceKmStr = "-";
|
||||||
|
long durationMin = 0;
|
||||||
|
String durationMinStr = "-";
|
||||||
|
String tolls = "-";
|
||||||
|
|
||||||
if (rCode == AMapException.CODE_AMAP_SUCCESS && result != null && !result.getPaths().isEmpty()) {
|
if (rCode == AMapException.CODE_AMAP_SUCCESS && result != null && !result.getPaths().isEmpty()) {
|
||||||
DrivePath path = result.getPaths().get(0);
|
|
||||||
|
|
||||||
// 规划成功,显示详情面板,隐藏模式选择
|
// 规划成功,显示详情面板,隐藏模式选择
|
||||||
|
if (detailPanel != null)
|
||||||
detailPanel.setVisibility(View.VISIBLE);
|
detailPanel.setVisibility(View.VISIBLE);
|
||||||
|
if (planToggleBtn != null)
|
||||||
planToggleBtn.setVisibility(View.GONE);
|
planToggleBtn.setVisibility(View.GONE);
|
||||||
|
if (mLocBtn != null)
|
||||||
|
mLocBtn.setVisibility(View.GONE);
|
||||||
|
if (modeMenu != null)
|
||||||
modeMenu.setVisibility(View.GONE);
|
modeMenu.setVisibility(View.GONE);
|
||||||
|
|
||||||
tvStationName.setText(endName);
|
tvStationName.setText(endName);
|
||||||
tvStationAddr.setText(endAddress);
|
tvStationAddr.setText(endAddress);
|
||||||
|
|
||||||
|
|
||||||
double distanceKm = path.getDistance() / 1000f;
|
if (truckRouteData != null) {
|
||||||
long durationMin = path.getDuration() / 60;
|
PathDto pathDto = truckRouteData.pathDto;
|
||||||
double tolls = path.getTolls();
|
if (pathDto != null) {
|
||||||
String hydrogenCost = truckRouteData.algorithmPath.hydrogenCost;
|
distanceKm = pathDto.distance / 1000f;
|
||||||
|
durationMin = pathDto.duration / 60;
|
||||||
|
tolls = pathDto.tolls + "元";
|
||||||
|
|
||||||
|
distanceKmStr = String.format("%.1f", distanceKm) + "公里";
|
||||||
|
durationMinStr = durationMin + "分钟";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 增加多级非空校验,防止点击搜索条目时崩溃
|
||||||
|
if (truckRouteData != null &&
|
||||||
|
truckRouteData.algorithmPath != null &&
|
||||||
|
truckRouteData.algorithmPath.hydrogenCost != null &&
|
||||||
|
!truckRouteData.algorithmPath.hydrogenCost.isEmpty()) {
|
||||||
|
hydrogenCost = truckRouteData.algorithmPath.hydrogenCost;
|
||||||
|
hydrogenCost = (isGetInputtips ? "--" : hydrogenCost) + "元";
|
||||||
|
}
|
||||||
|
|
||||||
|
tvDuration.setText("预计时间:" + durationMinStr);
|
||||||
|
tvDistance.setText("行驶里程:" + distanceKmStr);
|
||||||
|
tvTolls.setText("过路费:" + tolls);
|
||||||
|
tvTollsFuel.setText("预计加氢费用:" + hydrogenCost);
|
||||||
|
|
||||||
|
|
||||||
|
if (truckRouteData != null) {
|
||||||
|
DestinationSite destinationSite = truckRouteData.destinationSite;
|
||||||
|
if (destinationSite != null) {
|
||||||
|
startBusiness = destinationSite.startBusiness;
|
||||||
|
endBusiness = destinationSite.endBusiness;
|
||||||
|
hydrogenPrice = destinationSite.hydrogenPrice + "/L";
|
||||||
|
liaisonName = destinationSite.liaisonName;
|
||||||
|
liaisonPhone = destinationSite.liaisonPhone;
|
||||||
|
//开始结束时间
|
||||||
|
startBusiness = startBusiness + "-" + endBusiness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tvBusinessHours.setText("营业时间:" + startBusiness);
|
||||||
|
if (liaisonName != null && liaisonName.length() > 5) {
|
||||||
|
liaisonName = liaisonName.substring(0, 5) + "...";
|
||||||
|
}
|
||||||
|
tvPerson.setText("站联系人:" + liaisonName);
|
||||||
|
tvPrice.setText("加氢价格:" + hydrogenPrice);
|
||||||
|
tvPhone.setText("联系方式:" + liaisonPhone);
|
||||||
|
|
||||||
|
|
||||||
tvDuration.setText("预计时间:" + durationMin + "分钟");
|
|
||||||
tvDistance.setText("行驶里程:" + String.format("%.1f", distanceKm) + "公里");
|
|
||||||
tvTolls.setText("过路费:" + (int) tolls + "元");
|
|
||||||
tvTollsFuel.setText("加氢费用:" + hydrogenCost + "元");
|
|
||||||
|
|
||||||
aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(endPoint, 13f));
|
aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(endPoint, 13f));
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 规划失败回退面板
|
// 规划失败回退面板
|
||||||
searchArea.setVisibility(View.VISIBLE);
|
searchArea.setVisibility(View.VISIBLE);
|
||||||
Toast.makeText(mContext, "路径规划失败: " + rCode, Toast.LENGTH_SHORT).show();
|
Toast.makeText(mContext, "路径规划失败: " + rCode, Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void startRouteSearch() {
|
private void startRouteSearch() {
|
||||||
if (mActivity == null || startPoint == null) {
|
if (mActivity == null || startPoint == null) {
|
||||||
@@ -664,6 +653,8 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
//车辆信息
|
//车辆信息
|
||||||
AMapCarInfo carInfo = new AMapCarInfo();
|
AMapCarInfo carInfo = new AMapCarInfo();
|
||||||
carInfo.setCarNumber(plateNumber);
|
carInfo.setCarNumber(plateNumber);
|
||||||
|
|
||||||
|
if (truckRouteData != null) {
|
||||||
carInfo.setCarType(String.valueOf(truckRouteData.truckDto.mcarType));
|
carInfo.setCarType(String.valueOf(truckRouteData.truckDto.mcarType));
|
||||||
carInfo.setVehicleAxis(String.valueOf(truckRouteData.truckDto.mvehicleAxis));
|
carInfo.setVehicleAxis(String.valueOf(truckRouteData.truckDto.mvehicleAxis));
|
||||||
carInfo.setVehicleHeight(truckRouteData.truckDto.mvehicleHeight);
|
carInfo.setVehicleHeight(truckRouteData.truckDto.mvehicleHeight);
|
||||||
@@ -674,10 +665,17 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
carInfo.setVehicleWeight(truckRouteData.truckDto.mvehicleWeight);
|
carInfo.setVehicleWeight(truckRouteData.truckDto.mvehicleWeight);
|
||||||
carInfo.setRestriction(truckRouteData.truckDto.isRestriction);
|
carInfo.setRestriction(truckRouteData.truckDto.isRestriction);
|
||||||
carInfo.setVehicleLoadSwitch(truckRouteData.truckDto.mvehicleLoadSwitch);
|
carInfo.setVehicleLoadSwitch(truckRouteData.truckDto.mvehicleLoadSwitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 检查途径点数量,决定使用哪种导航方式
|
// 检查途径点数量,决定使用哪种导航方式
|
||||||
int wayPointsCount = waysPoiIds.size();
|
int wayPointsCount = waysPoiIds.size();
|
||||||
Log.d(TAG, "途经点数量: " + wayPointsCount);
|
Log.d(TAG, "途经点数量: " + wayPointsCount);
|
||||||
|
|
||||||
|
//如果是输入地址提示内容,不判断途经点
|
||||||
|
if (isGetInputtips) {
|
||||||
|
wayPointsCount = 0;
|
||||||
|
}
|
||||||
if (wayPointsCount > 3) {
|
if (wayPointsCount > 3) {
|
||||||
// 途经点超过3个,跳转到 NavigationActivity
|
// 途经点超过3个,跳转到 NavigationActivity
|
||||||
Intent intent = new Intent(mContext, NavigationActivity.class);
|
Intent intent = new Intent(mContext, NavigationActivity.class);
|
||||||
@@ -744,13 +742,11 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
* 获取货车路线算法信息
|
* 获取货车路线算法信息
|
||||||
*/
|
*/
|
||||||
private void fetchTruckRouteAlgorithm(AMapLocation loc) {
|
private void fetchTruckRouteAlgorithm(AMapLocation loc) {
|
||||||
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
if (plateNumber == null || plateNumber.isEmpty()) {
|
if (plateNumber == null || plateNumber.isEmpty()) {
|
||||||
|
Toast.makeText(mActivity,"请先绑定车辆后进行导航",Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
showLoading();
|
||||||
try {
|
try {
|
||||||
JSONObject json = new JSONObject();
|
JSONObject json = new JSONObject();
|
||||||
json.put("longitude", String.valueOf(loc.getLongitude()));
|
json.put("longitude", String.valueOf(loc.getLongitude()));
|
||||||
@@ -786,12 +782,17 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
|
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
|
|
||||||
|
mActivity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
// 开始规划前隐藏输入框面板
|
// 开始规划前隐藏输入框面板
|
||||||
searchArea.setVisibility(View.GONE);
|
searchArea.setVisibility(View.GONE);
|
||||||
|
|
||||||
RouteSearch.FromAndTo fromAndTo = new RouteSearch.FromAndTo(new LatLonPoint(startPoint.latitude, startPoint.longitude), new LatLonPoint(endPoint.latitude, endPoint.longitude));
|
RouteSearch.FromAndTo fromAndTo = new RouteSearch.FromAndTo(new LatLonPoint(startPoint.latitude, startPoint.longitude), new LatLonPoint(endPoint.latitude, endPoint.longitude));
|
||||||
RouteSearch.DriveRouteQuery query = new RouteSearch.DriveRouteQuery(fromAndTo, RouteSearch.DRIVING_SINGLE_DEFAULT, null, null, "");
|
RouteSearch.DriveRouteQuery query = new RouteSearch.DriveRouteQuery(fromAndTo, RouteSearch.DRIVING_SINGLE_DEFAULT, null, null, "");
|
||||||
routeSearch.calculateDriveRouteAsyn(query);
|
routeSearch.calculateDriveRouteAsyn(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
}
|
}
|
||||||
@@ -860,11 +861,18 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
aMap.setMyLocationEnabled(true);
|
aMap.setMyLocationEnabled(true);
|
||||||
aMap.setOnMarkerClickListener(this);
|
aMap.setOnMarkerClickListener(this);
|
||||||
|
|
||||||
// 添加地图触摸监听器,点击地图时隐藏提示列表
|
// 添加地图点击监听,当 detailPanel 显示时点击地图就执行 resetView
|
||||||
aMap.setOnMapClickListener(latLng -> {
|
aMap.setOnMapClickListener(latLng -> {
|
||||||
if (suggestionList != null && suggestionList.getVisibility() == View.VISIBLE) {
|
if (detailPanel != null && detailPanel.getVisibility() == View.VISIBLE) {
|
||||||
suggestionList.setVisibility(View.GONE);
|
resetView();
|
||||||
}
|
}
|
||||||
|
modeMenu.setVisibility(View.GONE);
|
||||||
|
});
|
||||||
|
aMap.setOnPOIClickListener(poi -> {
|
||||||
|
if (detailPanel != null && detailPanel.getVisibility() == View.VISIBLE) {
|
||||||
|
resetView();
|
||||||
|
}
|
||||||
|
modeMenu.setVisibility(View.GONE);
|
||||||
});
|
});
|
||||||
|
|
||||||
MyLocationStyle myLocationStyle = new MyLocationStyle();
|
MyLocationStyle myLocationStyle = new MyLocationStyle();
|
||||||
@@ -879,7 +887,7 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
}
|
}
|
||||||
|
|
||||||
myLocationStyle.anchor(0.5f, 0.5f);
|
myLocationStyle.anchor(0.5f, 0.5f);
|
||||||
myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER);
|
myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_SHOW);
|
||||||
aMap.setMyLocationStyle(myLocationStyle);
|
aMap.setMyLocationStyle(myLocationStyle);
|
||||||
aMap.getUiSettings().setZoomControlsEnabled(false);
|
aMap.getUiSettings().setZoomControlsEnabled(false);
|
||||||
aMap.getUiSettings().setLogoPosition(AMapOptions.LOGO_POSITION_BOTTOM_LEFT);
|
aMap.getUiSettings().setLogoPosition(AMapOptions.LOGO_POSITION_BOTTOM_LEFT);
|
||||||
@@ -910,6 +918,33 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
mlocationClient.stopLocation();
|
mlocationClient.stopLocation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDestination(String name, String address, double lat, double lon, String district) {
|
||||||
|
endName = name;
|
||||||
|
endAddress = address;
|
||||||
|
endPoint = new LatLng(lat, lon);
|
||||||
|
isUserSelectedDestination = true;
|
||||||
|
isGetInputtips = true;
|
||||||
|
|
||||||
|
Log.d(TAG, "setDestination called with name=" + name);
|
||||||
|
|
||||||
|
// 简化逻辑,直接设置文本
|
||||||
|
endInput.post(() -> {
|
||||||
|
try {
|
||||||
|
endInput.setText(district != null && !district.isEmpty() ? (name + " " + district ): name);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to set text to endInput", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Activity 实例
|
||||||
|
*/
|
||||||
|
public void setActivity(Activity activity) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
public void startLocation() {
|
public void startLocation() {
|
||||||
if (mlocationClient == null) {
|
if (mlocationClient == null) {
|
||||||
try {
|
try {
|
||||||
@@ -1064,6 +1099,7 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
dataMap.put("latLng", latLng);
|
dataMap.put("latLng", latLng);
|
||||||
dataMap.put("stationId", stationId);
|
dataMap.put("stationId", stationId);
|
||||||
dataMap.put("address", address);
|
dataMap.put("address", address);
|
||||||
|
dataMap.put("name", name);
|
||||||
m.setObject(dataMap);
|
m.setObject(dataMap);
|
||||||
|
|
||||||
stationMarkers.add(m);
|
stationMarkers.add(m);
|
||||||
@@ -1074,7 +1110,27 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
|
|
||||||
//地图选点
|
//地图选点
|
||||||
for (Marker m : stationMarkers) {
|
for (Marker m : stationMarkers) {
|
||||||
m.setIcon(BitmapDescriptorFactory.fromResource(m.equals(marker) ? R.drawable.ic_marker : R.drawable.ic_un_marker));
|
Object obj = m.getObject();
|
||||||
|
if (obj instanceof Map) {
|
||||||
|
Map<String, Object> dataMap = (Map<String, Object>) obj;
|
||||||
|
String name = (String) dataMap.get("name");
|
||||||
|
|
||||||
|
if (name != null) {
|
||||||
|
// 截取显示名称(最多7个字符)
|
||||||
|
String displayName = name.length() > 7 ? name.substring(0, 7) + "..." :
|
||||||
|
name;
|
||||||
|
boolean isSelected = m.equals(marker);
|
||||||
|
|
||||||
|
// 使用 getMarkerIconWithText 创建带文字的图标
|
||||||
|
BitmapDescriptor icon = getMarkerIconWithText(mContext, displayName,
|
||||||
|
isSelected);
|
||||||
|
m.setIcon(icon);
|
||||||
|
} else {
|
||||||
|
// 如果没有 name,降级到直接设置图标
|
||||||
|
m.setIcon(BitmapDescriptorFactory.fromResource(m.equals(marker) ?
|
||||||
|
R.drawable.ic_marker : R.drawable.ic_un_marker));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1099,6 +1155,9 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
// 更新 UI 和 业务逻辑
|
// 更新 UI 和 业务逻辑
|
||||||
endName = marker.getTitle();
|
endName = marker.getTitle();
|
||||||
isUserSelectedDestination = true; // 标识用户手动选择了目的地
|
isUserSelectedDestination = true; // 标识用户手动选择了目的地
|
||||||
|
isGetInputtips = false;
|
||||||
|
|
||||||
|
endInput.setText("");
|
||||||
|
|
||||||
// 需要传入当前位置以便接口计算路线
|
// 需要传入当前位置以便接口计算路线
|
||||||
if (mlocationClient != null && mlocationClient.getLastKnownLocation() != null) {
|
if (mlocationClient != null && mlocationClient.getLastKnownLocation() != null) {
|
||||||
@@ -1243,9 +1302,12 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
|
|
||||||
// 文本内容
|
// 文本内容
|
||||||
TextView tv = new TextView(mContext);
|
TextView tv = new TextView(mContext);
|
||||||
|
|
||||||
|
|
||||||
tv.setText(label + value);
|
tv.setText(label + value);
|
||||||
tv.setTextSize(14);
|
|
||||||
tv.setTextColor(Color.parseColor("#333333"));
|
tv.setTextSize(12);
|
||||||
|
tv.setTextColor(Color.parseColor("#ff1d2129"));
|
||||||
tv.setPadding(dp2px(6), 0, 0, 0);
|
tv.setPadding(dp2px(6), 0, 0, 0);
|
||||||
|
|
||||||
LinearLayout.LayoutParams tvParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f);
|
LinearLayout.LayoutParams tvParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f);
|
||||||
@@ -1258,23 +1320,6 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
return tv; // 返回TextView以便后续更新内容
|
return tv; // 返回TextView以便后续更新内容
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过载一个不需要weight的方法给第一行使用
|
|
||||||
private TextView createInfoItem(LinearLayout parent, int iconRes, String label, String value) {
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
||||||
|
|
||||||
ImageView iv = new ImageView(mContext);
|
|
||||||
iv.setImageResource(iconRes);
|
|
||||||
iv.setColorFilter(Color.parseColor("#017143"));
|
|
||||||
parent.addView(iv, new LinearLayout.LayoutParams(dp2px(18), dp2px(18)));
|
|
||||||
|
|
||||||
TextView tv = new TextView(mContext);
|
|
||||||
tv.setTextSize(14);
|
|
||||||
tv.setTextColor(Color.parseColor("#333333"));
|
|
||||||
tv.setPadding(dp2px(6), 0, 0, 0);
|
|
||||||
parent.addView(tv, params);
|
|
||||||
|
|
||||||
return tv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建模式菜单视图
|
// 创建模式菜单视图
|
||||||
private View createModeMenu(Context context) {
|
private View createModeMenu(Context context) {
|
||||||
@@ -1297,9 +1342,9 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
TextView item3 = createMenuItem(context, "加氢规划模式", true);
|
TextView item3 = createMenuItem(context, "加氢规划模式", true);
|
||||||
item3.setOnClickListener(v -> switchMode("加氢规划"));
|
item3.setOnClickListener(v -> switchMode("加氢规划"));
|
||||||
|
|
||||||
menu.addView(item1);
|
|
||||||
menu.addView(item2);
|
|
||||||
menu.addView(item3);
|
menu.addView(item3);
|
||||||
|
menu.addView(item2);
|
||||||
|
menu.addView(item1);
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1310,13 +1355,12 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
tv.setPadding(dp2px(15), dp2px(12), dp2px(15), dp2px(12));
|
tv.setPadding(dp2px(15), dp2px(12), dp2px(15), dp2px(12));
|
||||||
tv.setGravity(Gravity.CENTER);
|
tv.setGravity(Gravity.CENTER);
|
||||||
if (isHighlight) {
|
if (isHighlight) {
|
||||||
tv.setTextColor(Color.WHITE);
|
|
||||||
// 顶部圆角绿色背景
|
// 顶部圆角绿色背景
|
||||||
tv.setBackground(getTopRoundedDrawable(Color.parseColor("#27AE60"), 12));
|
tv.setBackground(getTopRoundedDrawable(Color.parseColor("#27AE60"), 12));
|
||||||
} else {
|
} else {
|
||||||
tv.setTextColor(Color.parseColor("#666666"));
|
|
||||||
tv.setBackgroundColor(Color.TRANSPARENT);
|
tv.setBackgroundColor(Color.TRANSPARENT);
|
||||||
}
|
}
|
||||||
|
tv.setTextColor(Color.parseColor(isHighlight ? "#ffffffff" : "#ffc9cdd4"));
|
||||||
return tv;
|
return tv;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1400,6 +1444,9 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
destinationSite.longitude = siteJson.optString("longitude", "");
|
destinationSite.longitude = siteJson.optString("longitude", "");
|
||||||
destinationSite.latitude = siteJson.optString("latitude", "");
|
destinationSite.latitude = siteJson.optString("latitude", "");
|
||||||
destinationSite.distance = siteJson.optString("distance", "");
|
destinationSite.distance = siteJson.optString("distance", "");
|
||||||
|
destinationSite.hydrogenPrice = siteJson.optString("hydrogenPrice", "");
|
||||||
|
destinationSite.liaisonName = siteJson.optString("liaisonName", "");
|
||||||
|
destinationSite.liaisonPhone = siteJson.optString("liaisonPhone", "");
|
||||||
truckRouteData.destinationSite = destinationSite;
|
truckRouteData.destinationSite = destinationSite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1597,5 +1644,9 @@ public class NativeMapView implements PlatformView, LocationSource, AMapLocation
|
|||||||
public String longitude;
|
public String longitude;
|
||||||
public String latitude;
|
public String latitude;
|
||||||
public String distance;
|
public String distance;
|
||||||
|
|
||||||
|
public String hydrogenPrice;
|
||||||
|
public String liaisonName;
|
||||||
|
public String liaisonPhone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ public class NavigationActivity extends ComponentActivity implements AMapNaviLis
|
|||||||
mAMapNavi.setCarInfo(carInfo);
|
mAMapNavi.setCarInfo(carInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mAMapNavi.setUseInnerVoice(true);
|
||||||
// 计算并启动导航
|
// 计算并启动导航
|
||||||
mAMapNavi.calculateDriveRoute(startNaviPoi, endNaviPoi, wayPoints, PathPlanningStrategy.DRIVING_MULTIPLE_ROUTES_DEFAULT);
|
mAMapNavi.calculateDriveRoute(startNaviPoi, endNaviPoi, wayPoints, PathPlanningStrategy.DRIVING_MULTIPLE_ROUTES_DEFAULT);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
package com.lnkj.ln_jq_app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.amap.api.services.help.Inputtips;
|
||||||
|
import com.amap.api.services.help.InputtipsQuery;
|
||||||
|
import com.amap.api.services.help.Tip;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索目的地页面
|
||||||
|
*/
|
||||||
|
public class SearchDestinationActivity extends ComponentActivity implements Inputtips.InputtipsListener {
|
||||||
|
private static final String TAG = "SearchDestinationActivity";
|
||||||
|
|
||||||
|
public static final String EXTRA_RESULT_NAME = "result_name";
|
||||||
|
public static final String EXTRA_RESULT_ADDRESS = "result_address";
|
||||||
|
public static final String EXTRA_RESULT_LAT = "result_lat";
|
||||||
|
public static final String EXTRA_RESULT_LON = "result_lon";
|
||||||
|
public static final String EXTRA_RESULT_DISTRICT = "result_district";
|
||||||
|
|
||||||
|
// 静态回调接口
|
||||||
|
public interface DestinationCallback {
|
||||||
|
void onDestinationSelected(String name, String address, double lat, double lon, String district);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DestinationCallback callback;
|
||||||
|
|
||||||
|
public static void setCallback(DestinationCallback callback) {
|
||||||
|
SearchDestinationActivity.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EditText searchInput;
|
||||||
|
private ListView suggestionList;
|
||||||
|
private ImageView backBtn;
|
||||||
|
|
||||||
|
private ArrayAdapter<String> suggestionAdapter;
|
||||||
|
private List<Tip> currentTipList;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setContentView(R.layout.activity_search_destination);
|
||||||
|
|
||||||
|
initViews();
|
||||||
|
setupListeners();
|
||||||
|
|
||||||
|
// 自动显示键盘
|
||||||
|
showKeyboard();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error in onCreate", e);
|
||||||
|
e.printStackTrace();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initViews() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "initViews started");
|
||||||
|
searchInput = findViewById(R.id.search_input);
|
||||||
|
suggestionList = findViewById(R.id.suggestion_list);
|
||||||
|
backBtn = findViewById(R.id.back_btn);
|
||||||
|
|
||||||
|
currentTipList = new ArrayList<>();
|
||||||
|
|
||||||
|
Log.d(TAG, "initViews completed: searchInput=" + (searchInput != null ? "ok" : "null") + ", suggestionList=" + (suggestionList != null ? "ok" : "null") + ", backBtn=" + (backBtn != null ? "ok" : "null"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error in initViews", e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestionAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, new ArrayList<>()) {
|
||||||
|
@Override
|
||||||
|
public View getView(int position, android.view.View convertView, android.view.ViewGroup parent) {
|
||||||
|
View view = super.getView(position, convertView, parent);
|
||||||
|
TextView text = view.findViewById(android.R.id.text1);
|
||||||
|
text.setTextColor(Color.parseColor("#333333"));
|
||||||
|
text.setTextSize(14);
|
||||||
|
view.setBackgroundColor(Color.TRANSPARENT);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
suggestionList.setAdapter(suggestionAdapter);
|
||||||
|
suggestionList.setDivider(new ColorDrawable(Color.LTGRAY));
|
||||||
|
suggestionList.setDividerHeight(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void setupListeners() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "setupListeners started");
|
||||||
|
|
||||||
|
// 返回按钮
|
||||||
|
backBtn.setOnClickListener(v -> {
|
||||||
|
Log.d(TAG, "Back button clicked");
|
||||||
|
hideKeyboard();
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文本变化监听
|
||||||
|
searchInput.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
Log.d(TAG, "Text changed: " + (s != null ? s.toString() : "null") + ", length=" + (s != null ? s.length() : 0));
|
||||||
|
if (s.length() > 1) {
|
||||||
|
searchLocation(s.toString());
|
||||||
|
} else {
|
||||||
|
suggestionList.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 列表点击
|
||||||
|
suggestionList.setOnItemClickListener((parent, view, position, id) -> {
|
||||||
|
Log.d(TAG, "Suggestion clicked at position: " + position);
|
||||||
|
try {
|
||||||
|
Tip tip = currentTipList.get(position);
|
||||||
|
if (tip.getPoint() != null) {
|
||||||
|
String name = tip.getName();
|
||||||
|
String address = tip.getAddress();
|
||||||
|
String district = tip.getDistrict();
|
||||||
|
double lat = tip.getPoint().getLatitude();
|
||||||
|
double lon = tip.getPoint().getLongitude();
|
||||||
|
|
||||||
|
Log.d(TAG, "Selected destination: " + name + ", lat=" + lat + ", lon=" + lon);
|
||||||
|
|
||||||
|
// 优先使用静态回调
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onDestinationSelected(name, address, lat, lon, district);
|
||||||
|
} else {
|
||||||
|
// 降级使用 Intent 回调
|
||||||
|
Intent result = new Intent();
|
||||||
|
result.putExtra(EXTRA_RESULT_NAME, name);
|
||||||
|
result.putExtra(EXTRA_RESULT_ADDRESS, address);
|
||||||
|
result.putExtra(EXTRA_RESULT_LAT, lat);
|
||||||
|
result.putExtra(EXTRA_RESULT_LON, lon);
|
||||||
|
result.putExtra(EXTRA_RESULT_DISTRICT, district);
|
||||||
|
setResult(RESULT_OK, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideKeyboard();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error handling suggestion click", e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Log.d(TAG, "setupListeners completed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error in setupListeners", e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void searchLocation(String keyword) {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Searching for: " + keyword);
|
||||||
|
InputtipsQuery query = new InputtipsQuery(keyword, "");
|
||||||
|
query.setCityLimit(false);
|
||||||
|
Inputtips inputtips = new Inputtips(this, query);
|
||||||
|
inputtips.setInputtipsListener(this);
|
||||||
|
inputtips.requestInputtipsAsyn();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error in searchLocation", e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGetInputtips(List<Tip> tipList, int rCode) {
|
||||||
|
Log.d(TAG, "onGetInputtips called, rCode=" + rCode + ", tipList size=" + (tipList != null ? tipList.size() : "null"));
|
||||||
|
if (rCode == 1000 && tipList != null && !tipList.isEmpty()) {
|
||||||
|
currentTipList.clear();
|
||||||
|
currentTipList.addAll(tipList);
|
||||||
|
|
||||||
|
List<String> suggestionNames = new ArrayList<>();
|
||||||
|
for (Tip tip : tipList) {
|
||||||
|
String name = tip.getName();
|
||||||
|
String district = tip.getDistrict();
|
||||||
|
String displayText = district != null && !district.isEmpty() ? name + " " + district : name;
|
||||||
|
suggestionNames.add(displayText);
|
||||||
|
}
|
||||||
|
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
suggestionAdapter.clear();
|
||||||
|
suggestionAdapter.addAll(suggestionNames);
|
||||||
|
suggestionAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
|
if (suggestionNames.size() > 0) {
|
||||||
|
suggestionList.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
suggestionList.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
suggestionList.setVisibility(View.GONE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void showKeyboard() {
|
||||||
|
searchInput.requestFocus();
|
||||||
|
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
if (imm != null) {
|
||||||
|
imm.showSoftInput(searchInput, InputMethodManager.SHOW_IMPLICIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideKeyboard() {
|
||||||
|
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
if (imm != null) {
|
||||||
|
imm.hideSoftInputFromWindow(searchInput.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPointerCaptureChanged(boolean hasCapture) {
|
||||||
|
super.onPointerCaptureChanged(hasCapture);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
// 清理回调,避免内存泄漏
|
||||||
|
callback = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ln_jq_app/android/app/src/main/res/drawable/back.png
Normal file
|
After Width: | Height: | Size: 705 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- 背景颜色:使用浅灰色 -->
|
||||||
|
<solid android:color="#ffffffff" />
|
||||||
|
<!-- 圆角:8dp左右 -->
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<!-- 边框:可选,如果需要更清晰的边缘可以加上 -->
|
||||||
|
<stroke android:width="1dp" android:color="#EAEAEA" />
|
||||||
|
</shape>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
40
ln_jq_app/android/app/src/main/res/drawable/ic_fuel.xml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8,14.667C10.946,14.667 13.333,12.496 13.333,9.818C13.333,7.192 11.556,4.364 8,1.333C4.444,4.364 2.667,7.192 2.667,9.818C2.667,12.496 5.054,14.667 8,14.667Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M6.286,6.333L8,8.123L9.714,6.333"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M6,8.719H10"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M6,10.509H10"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8,8.719V12"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
41
ln_jq_app/android/app/src/main/res/drawable/ic_mileage.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M3.667,1.375L2,13.375"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M12.333,1.375L13.988,13.36"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8,1.375V3.375"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8,11.042V13.375"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8,6.042V8.375"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
13
ln_jq_app/android/app/src/main/res/drawable/ic_path.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h24v24h-24z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M6.518,10.241C4.181,10.241 2.29,12.135 2.29,14.469C2.29,16.804 6.518,21.684 6.518,21.684C6.518,21.684 10.746,16.806 10.746,14.469C10.746,12.133 8.853,10.241 6.518,10.241ZM6.518,15.616C5.853,15.616 5.313,15.077 5.313,14.411C5.313,13.745 5.853,13.206 6.518,13.206C7.184,13.206 7.723,13.745 7.723,14.411C7.723,15.077 7.181,15.616 6.518,15.616ZM19.676,2.301C18.567,2.301 17.67,3.198 17.67,4.307C17.67,5.416 19.676,7.729 19.676,7.729C19.676,7.729 21.682,5.416 21.682,4.307C21.682,3.198 20.785,2.301 19.676,2.301ZM19.676,4.851C19.36,4.851 19.104,4.595 19.104,4.279C19.104,3.963 19.36,3.707 19.676,3.707C19.992,3.707 20.248,3.963 20.248,4.279C20.246,4.595 19.99,4.851 19.676,4.851ZM18.026,13.185C17.386,12.813 16.629,12.674 15.898,12.538C14.845,12.344 14.196,12.191 14.074,11.72C14.051,11.627 14.037,11.493 14.156,11.291C14.325,11.005 14.855,10.422 16.519,9.735C16.993,9.539 17.449,9.382 17.801,9.27C18.168,9.153 18.393,8.768 18.296,8.395C18.199,8.019 17.812,7.792 17.442,7.909C17.056,8.03 16.543,8.205 15.999,8.43C14.431,9.074 13.404,9.798 12.945,10.579C12.663,11.057 12.584,11.575 12.713,12.077C12.877,12.709 13.303,13.185 13.978,13.485C14.482,13.71 15.073,13.818 15.642,13.923C16.256,14.036 16.889,14.153 17.318,14.402C17.597,14.563 17.888,14.819 17.916,15.487C17.958,16.427 16.88,17.448 14.881,18.367C13.833,18.848 12.764,19.192 12.099,19.385C11.729,19.493 11.5,19.869 11.589,20.244C11.68,20.625 12.064,20.86 12.441,20.752C13.144,20.55 14.295,20.183 15.45,19.654C16.547,19.152 17.424,18.606 18.054,18.032C18.938,17.228 19.364,16.349 19.322,15.426C19.278,14.413 18.842,13.661 18.026,13.185Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
20
ln_jq_app/android/app/src/main/res/drawable/ic_person.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8,6.042C9.289,6.042 10.333,4.997 10.333,3.708C10.333,2.42 9.289,1.375 8,1.375C6.711,1.375 5.667,2.42 5.667,3.708C5.667,4.997 6.711,6.042 8,6.042Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M2,12.975V13.375H14V12.975C14,11.481 14,10.735 13.709,10.164C13.454,9.663 13.046,9.255 12.544,8.999C11.974,8.708 11.227,8.708 9.733,8.708H6.267C4.773,8.708 4.026,8.708 3.456,8.999C2.954,9.255 2.546,9.663 2.291,10.164C2,10.735 2,11.481 2,12.975Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
12
ln_jq_app/android/app/src/main/res/drawable/ic_phone.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M5.665,1.937C5.908,1.937 6.131,2.068 6.248,2.28L7.064,3.749C7.171,3.941 7.176,4.174 7.077,4.371L6.292,5.942C6.292,5.942 6.519,7.112 7.472,8.065C8.425,9.018 9.591,9.242 9.591,9.242L11.162,8.456C11.359,8.358 11.592,8.363 11.785,8.47L13.258,9.289C13.469,9.407 13.6,9.63 13.6,9.872V11.563C13.6,12.424 12.8,13.046 11.984,12.771C10.308,12.205 7.707,11.128 6.058,9.479C4.409,7.831 3.332,5.229 2.767,3.553C2.491,2.737 3.113,1.937 3.975,1.937H5.665Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"/>
|
||||||
|
</vector>
|
||||||
40
ln_jq_app/android/app/src/main/res/drawable/ic_price.xml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8,14.042C11.682,14.042 14.667,11.057 14.667,7.375C14.667,3.693 11.682,0.708 8,0.708C4.318,0.708 1.333,3.693 1.333,7.375C1.333,11.057 4.318,14.042 8,14.042Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M6,6.708H10"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M6,8.708H10"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8.003,6.708V10.708"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M10,4.375L8,6.375L6,4.375"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
26
ln_jq_app/android/app/src/main/res/drawable/ic_search.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="18dp"
|
||||||
|
android:height="18dp"
|
||||||
|
android:viewportWidth="18"
|
||||||
|
android:viewportHeight="18">
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M7.875,14.5C11.396,14.5 14.25,11.646 14.25,8.125C14.25,4.604 11.396,1.75 7.875,1.75C4.354,1.75 1.5,4.604 1.5,8.125C1.5,11.646 4.354,14.5 7.875,14.5Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#4E5969"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M9.997,5.629C9.454,5.086 8.704,4.75 7.875,4.75C7.047,4.75 6.297,5.086 5.754,5.629"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#4E5969"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M12.458,12.707L15.64,15.889"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#4E5969"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 810 B |
19
ln_jq_app/android/app/src/main/res/drawable/ic_time.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8,14.042C11.682,14.042 14.667,11.057 14.667,7.375C14.667,3.693 11.682,0.708 8,0.708C4.318,0.708 1.333,3.693 1.333,7.375C1.333,11.057 4.318,14.042 8,14.042Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8.003,3.375L8.003,7.378L10.829,10.204"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 924 B |
37
ln_jq_app/android/app/src/main/res/drawable/ic_toll.xml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M1.333,2.042H3.667C3.667,2.042 4,3.708 5.667,3.708C7.333,3.708 7.667,2.042 7.667,2.042H14.667V12.708H7.667C7.667,12.708 7.333,11.042 5.667,11.042C4,11.042 3.667,12.708 3.667,12.708H1.333V2.042Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M5.667,5.708V6.375"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M5.667,8.375V9.042"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8.333,6.375H12"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8.333,8.375H12"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#017137"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#FFFFFF" />
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="24dp"
|
||||||
|
android:topRightRadius="24dp"
|
||||||
|
android:bottomLeftRadius="0dp"
|
||||||
|
android:bottomRightRadius="0dp" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#FFFFFF"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="55dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/back_btn"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:src="@drawable/back" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="15dp"
|
||||||
|
android:text="选择地点"
|
||||||
|
android:textColor="#000000"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/search_input"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_margin="12dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:hint="输入地址"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:textColorHint="#999999"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/suggestion_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -21,11 +21,9 @@
|
|||||||
.amap-callamap,
|
.amap-callamap,
|
||||||
.amap-lib-driving-callBtn,
|
.amap-lib-driving-callBtn,
|
||||||
.amap-copyright,
|
.amap-copyright,
|
||||||
.amap-logo {
|
.amap-logo{bottom: 60px}
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 去除高德默认的 label 边框和背景 */
|
/* 去除高德默认的 label 边框 and 背景 */
|
||||||
.amap-marker-label {
|
.amap-marker-label {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
@@ -109,7 +107,7 @@
|
|||||||
/* --- 导航结果面板 (底部弹出) --- */
|
/* --- 导航结果面板 (底部弹出) --- */
|
||||||
#panel {
|
#panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 75px;
|
bottom: 95px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 35%;
|
height: 35%;
|
||||||
@@ -129,7 +127,7 @@
|
|||||||
#location-btn {
|
#location-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
bottom: 75px;
|
bottom: 105px;
|
||||||
/* 默认位置 */
|
/* 默认位置 */
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
@@ -159,7 +157,7 @@
|
|||||||
/* --- 调整比例尺位置 --- */
|
/* --- 调整比例尺位置 --- */
|
||||||
.amap-scalecontrol {
|
.amap-scalecontrol {
|
||||||
/* 初始状态:避开底部的定位按钮或留出安全间距 */
|
/* 初始状态:避开底部的定位按钮或留出安全间距 */
|
||||||
bottom: 80px !important;
|
bottom: 110px !important;
|
||||||
left: 10px !important;
|
left: 10px !important;
|
||||||
transition: bottom 0.3s ease;
|
transition: bottom 0.3s ease;
|
||||||
/* 增加平滑动画 */
|
/* 增加平滑动画 */
|
||||||
@@ -195,10 +193,10 @@
|
|||||||
|
|
||||||
<div id="search-box">
|
<div id="search-box">
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<input id="startInput" placeholder="起点: 请输入当前地点" />
|
<input id="startInput" placeholder="起点: 请输入当前地点" onfocus="this.select()" />
|
||||||
</div>
|
</div>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<input id="endInput" placeholder="终点: 请输入目的地" />
|
<input id="endInput" placeholder="终点: 请输入目的地" onfocus="this.select()" />
|
||||||
<button onclick="startRouteSearch()">路径规划</button>
|
<button onclick="startRouteSearch()">路径规划</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,6 +219,7 @@
|
|||||||
var currentLat, currentLng;
|
var currentLat, currentLng;
|
||||||
var isTruckMode = false;
|
var isTruckMode = false;
|
||||||
var isInitialLocationSet = false;
|
var isInitialLocationSet = false;
|
||||||
|
var stationMarkers = []; // 存储所有站点的标记
|
||||||
|
|
||||||
|
|
||||||
function initMap() {
|
function initMap() {
|
||||||
@@ -243,6 +242,11 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 点击地图空白处重置状态
|
||||||
|
map.on('click', function() {
|
||||||
|
resetSearchState();
|
||||||
|
});
|
||||||
|
|
||||||
// 添加基础控件
|
// 添加基础控件
|
||||||
map.addControl(new AMap.Scale());
|
map.addControl(new AMap.Scale());
|
||||||
map.addControl(new AMap.ToolBar({
|
map.addControl(new AMap.ToolBar({
|
||||||
@@ -284,6 +288,19 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置搜索状态,隐藏面板和路线
|
||||||
|
*/
|
||||||
|
function resetSearchState() {
|
||||||
|
if (document.body.classList.contains('panel-active')) {
|
||||||
|
console.log("JS->: 重置地图状态");
|
||||||
|
document.body.classList.remove('panel-active');
|
||||||
|
var panel = document.getElementById('panel');
|
||||||
|
panel.style.display = 'none';
|
||||||
|
if (driving) driving.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 核心功能 1: 接收 Flutter 传来的定位数据
|
* 核心功能 1: 接收 Flutter 传来的定位数据
|
||||||
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
|
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
|
||||||
@@ -336,6 +353,8 @@
|
|||||||
fetchStationInfo(addressComponent.province, addressComponent.city,
|
fetchStationInfo(addressComponent.province, addressComponent.city,
|
||||||
addressComponent.district, lat, lng);
|
addressComponent.district, lat, lng);
|
||||||
|
|
||||||
|
fetchStationInfoList(lat, lng);
|
||||||
|
|
||||||
// 策略1: 优先使用最近的、类型合适的POI的名称
|
// 策略1: 优先使用最近的、类型合适的POI的名称
|
||||||
if (pois && pois.length > 0) {
|
if (pois && pois.length > 0) {
|
||||||
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI,这类POI通常是具体的建筑或地点名
|
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI,这类POI通常是具体的建筑或地点名
|
||||||
@@ -397,7 +416,6 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
// "asoco-token": "e28eada8-4611-4dc2-a942-0122e52f52da"
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
province: province,
|
province: province,
|
||||||
@@ -437,6 +455,79 @@
|
|||||||
.catch(err => console.error('JS->:获取站点信息失败:', err));
|
.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 () {
|
||||||
|
var stationName = station.name || "目的地";
|
||||||
|
document.getElementById('endInput').value = station.address ||
|
||||||
|
stationName;
|
||||||
|
|
||||||
|
// 更新当前的 destMarker
|
||||||
|
if (destMarker && destMarker !== sMarker) destMarker.setMap(null);
|
||||||
|
destMarker = sMarker;
|
||||||
|
|
||||||
|
// 直接传入坐标对象,避免关键字搜索失败
|
||||||
|
var loc = new AMap.LngLat(station.longitude, station.latitude);
|
||||||
|
startRouteSearch(loc);
|
||||||
|
});
|
||||||
|
|
||||||
|
stationMarkers.push(sMarker);
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.log("JS->: 业务报错或无数据:", res.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('JS->:获取站点信息失败:', err));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 地理编码并在地图标记终点
|
* 地理编码并在地图标记终点
|
||||||
*/
|
*/
|
||||||
@@ -447,7 +538,6 @@
|
|||||||
if (destMarker) destMarker.setMap(null);
|
if (destMarker) destMarker.setMap(null);
|
||||||
|
|
||||||
// 2. 创建自定义图标
|
// 2. 创建自定义图标
|
||||||
// 假设图标大小为 32x32,你可以根据实际图片尺寸调整 Size
|
|
||||||
var destIcon = new AMap.Icon({
|
var destIcon = new AMap.Icon({
|
||||||
size: new AMap.Size(32, 32), // 图标尺寸
|
size: new AMap.Size(32, 32), // 图标尺寸
|
||||||
image: 'ic_tag.png', // 本地图片路径
|
image: 'ic_tag.png', // 本地图片路径
|
||||||
@@ -459,8 +549,6 @@
|
|||||||
map: map,
|
map: map,
|
||||||
position: [longitude, latitude],
|
position: [longitude, latitude],
|
||||||
icon: destIcon, // 使用自定义图标
|
icon: destIcon, // 使用自定义图标
|
||||||
// 偏移量:如果图标底部中心是尖角,offset 设为宽的一半的负数,高度的负数
|
|
||||||
// 这样能确保图片的底部尖端指向地图上的精确位置
|
|
||||||
offset: new AMap.Pixel(-16, -32),
|
offset: new AMap.Pixel(-16, -32),
|
||||||
title: name,
|
title: name,
|
||||||
label: {
|
label: {
|
||||||
@@ -469,17 +557,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. 打印调试信息
|
console.log("JS->: 终点标记已添加", address);
|
||||||
console.log("JS->: 终点标记已添加", address, loc.toString());
|
|
||||||
|
|
||||||
// 5. 自动调整视野包含起点和终点
|
|
||||||
// if (marker) {
|
|
||||||
// // 如果起点标志已存在,缩放地图以展示两者
|
|
||||||
// map.setFitView([marker, destMarker], false, [60, 60, 60, 60]);
|
|
||||||
// } else {
|
|
||||||
// // 如果没有起点,直接跳到终点
|
|
||||||
// map.setCenter(loc);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -498,11 +576,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径规划
|
* 路径规划
|
||||||
|
* @param {AMap.LngLat} [destLoc] 可选的终点坐标
|
||||||
*/
|
*/
|
||||||
function startRouteSearch() {
|
function startRouteSearch(destLoc) {
|
||||||
// 获取输入框的文字
|
|
||||||
var startKw = document.getElementById('startInput').value;
|
var startKw = document.getElementById('startInput').value;
|
||||||
var endKw = document.getElementById('endInput').value;
|
var endKw = document.getElementById('endInput').value;
|
||||||
|
|
||||||
@@ -510,63 +589,59 @@
|
|||||||
alert("请输入起点");
|
alert("请输入起点");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!endKw) {
|
if (!endKw) {
|
||||||
alert("请输入终点");
|
alert("请输入终点");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除旧路线
|
|
||||||
if (driving) driving.clear();
|
if (driving) driving.clear();
|
||||||
|
|
||||||
// 收起键盘
|
|
||||||
document.getElementById('startInput').blur();
|
document.getElementById('startInput').blur();
|
||||||
document.getElementById('endInput').blur();
|
document.getElementById('endInput').blur();
|
||||||
|
|
||||||
// --- 构造路径规划的点 (使用数组方式,更灵活) ---
|
|
||||||
var points = [];
|
var points = [];
|
||||||
|
|
||||||
// 1. 处理起点逻辑
|
// 1. 起点逻辑
|
||||||
// 如果输入框是空的,或者写着 "我的位置",则使用 GPS 坐标
|
if (!startKw || startKw === '我的位置' || startKw.includes('当前位置')) {
|
||||||
if (!startKw || startKw === '我的位置') {
|
|
||||||
if (!currentLng || !currentLat) {
|
if (!currentLng || !currentLat) {
|
||||||
// 如果还没获取到定位
|
if (window.flutter_inappwebview) window.flutter_inappwebview.callHandler('requestLocation');
|
||||||
if (window.flutter_inappwebview) {
|
|
||||||
window.flutter_inappwebview.callHandler('requestLocation');
|
|
||||||
}
|
|
||||||
alert("正在获取定位,请稍后...");
|
alert("正在获取定位,请稍后...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 使用精准坐标对象 (避免高德去猜 '我的位置' 关键词)
|
|
||||||
points.push({
|
points.push({
|
||||||
keyword: '我的位置', // 用于显示的名字
|
keyword: '我的位置',
|
||||||
location: new AMap.LngLat(currentLng, currentLat) // 实际导航用的坐标
|
location: new AMap.LngLat(currentLng, currentLat)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 如果用户手动输入了地点 (例如 "北京南站")
|
|
||||||
// 直接存入关键词,让高德自己去搜
|
|
||||||
points.push({
|
points.push({
|
||||||
keyword: startKw
|
keyword: startKw
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 处理终点逻辑 (通常是关键词)
|
// 2. 终点逻辑:如果有传入坐标,则直接使用坐标导航,成功率最高
|
||||||
|
if (destLoc) {
|
||||||
|
points.push({
|
||||||
|
keyword: endKw,
|
||||||
|
location: destLoc // 关键:使用精确坐标
|
||||||
|
});
|
||||||
|
} else {
|
||||||
points.push({
|
points.push({
|
||||||
keyword: endKw
|
keyword: endKw
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 发起搜索
|
// 3. 发起搜索
|
||||||
// points 数组里现在是一个起点对象和一个终点对象
|
|
||||||
driving.search(points, function (status, result) {
|
driving.search(points, function (status, result) {
|
||||||
if (status === 'complete') {
|
if (status === 'complete') {
|
||||||
console.log('JS: 规划成功');
|
console.log('JS: 规划成功');
|
||||||
var panel = document.getElementById('panel');
|
var panel = document.getElementById('panel');
|
||||||
panel.style.display = 'block';
|
panel.style.display = 'block';
|
||||||
document.body.classList.add('panel-active');
|
document.body.classList.add('panel-active');
|
||||||
} else {
|
|
||||||
console.log('JS: 规划失败', result);
|
|
||||||
alert("规划失败,请检查起终点名称");
|
|
||||||
}
|
}
|
||||||
|
// else {
|
||||||
|
// console.error('JS: 规划失败', result);
|
||||||
|
// // 如果坐标规划都失败了,通常是由于起终点距离过近或政策限制(如货车禁行)
|
||||||
|
// alert("路径规划未成功,请尝试微调起终点");
|
||||||
|
// }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
ln_jq_app/assets/images/history_bg.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
ln_jq_app/assets/images/ic_attention@2x.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
ln_jq_app/assets/images/ic_close@2x.png
Normal file
|
After Width: | Height: | Size: 892 B |
BIN
ln_jq_app/assets/images/ic_ex_menu@2x.png
Normal file
|
After Width: | Height: | Size: 394 B |
|
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 947 B |
|
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 594 B |
BIN
ln_jq_app/assets/images/ic_serch@2x.png
Normal file
|
After Width: | Height: | Size: 636 B |
BIN
ln_jq_app/assets/images/ic_upload@2x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
ln_jq_app/assets/images/mall_bar@2x.png
Normal file
|
After Width: | Height: | Size: 896 B |
BIN
ln_jq_app/assets/images/mall_pay_success@2x.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
ln_jq_app/assets/images/rule_bg@2x.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
ln_jq_app/assets/images/rule_bg_1@2x.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
ln_jq_app/assets/images/tips_1@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
ln_jq_app/assets/images/tips_2@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
ln_jq_app/assets/images/tips_3@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
ln_jq_app/assets/images/tips_4@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
ln_jq_app/assets/images/tips_5@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 545 KiB |
@@ -26,6 +26,8 @@ PODS:
|
|||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
|
- flutter_app_update (0.0.1):
|
||||||
|
- Flutter
|
||||||
- flutter_inappwebview_ios (0.0.1):
|
- flutter_inappwebview_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_inappwebview_ios/Core (= 0.0.1)
|
- flutter_inappwebview_ios/Core (= 0.0.1)
|
||||||
@@ -45,14 +47,16 @@ PODS:
|
|||||||
- Masonry (1.1.0)
|
- Masonry (1.1.0)
|
||||||
- MBProgressHUD (1.2.0)
|
- MBProgressHUD (1.2.0)
|
||||||
- MJExtension (3.4.2)
|
- MJExtension (3.4.2)
|
||||||
- mobile_scanner (7.0.0):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- OrderedSet (6.0.3)
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- path_provider_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- saver_gallery (0.0.1):
|
||||||
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -65,14 +69,16 @@ DEPENDENCIES:
|
|||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
|
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
||||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
|
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
|
||||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
|
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
@@ -103,6 +109,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
|
flutter_app_update:
|
||||||
|
:path: ".symlinks/plugins/flutter_app_update/ios"
|
||||||
flutter_inappwebview_ios:
|
flutter_inappwebview_ios:
|
||||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
@@ -113,12 +121,14 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
mobile_scanner:
|
|
||||||
:path: ".symlinks/plugins/mobile_scanner/darwin"
|
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
|
path_provider_foundation:
|
||||||
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
|
saver_gallery:
|
||||||
|
:path: ".symlinks/plugins/saver_gallery/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
@@ -128,29 +138,31 @@ SPEC CHECKSUMS:
|
|||||||
AlicloudELS: fbf821383330465a5af84a033f36f263ae46ca41
|
AlicloudELS: fbf821383330465a5af84a033f36f263ae46ca41
|
||||||
AlicloudPush: 52cbf38ffc20c07f039cbc72d5738745fd986215
|
AlicloudPush: 52cbf38ffc20c07f039cbc72d5738745fd986215
|
||||||
AlicloudUTDID: 5d2f22d50e11eecd38f30bc7a48c71925ea90976
|
AlicloudUTDID: 5d2f22d50e11eecd38f30bc7a48c71925ea90976
|
||||||
aliyun_push_flutter: ab0bf7112ef3797f506770a7a9f47f004635a9f6
|
aliyun_push_flutter: 0fc2f048a08687ef256c0cfdd72dd7a550ef3347
|
||||||
AMapFoundation-NO-IDFA: 6ce0ef596d4eb8d934ff498e56747b6de1247b05
|
AMapFoundation-NO-IDFA: 6ce0ef596d4eb8d934ff498e56747b6de1247b05
|
||||||
AMapLocation-NO-IDFA: 590fd42af0c8ea9eac26978348221bbc16be4ef9
|
AMapLocation-NO-IDFA: 590fd42af0c8ea9eac26978348221bbc16be4ef9
|
||||||
AMapNavi-NO-IDFA: 22edfa7d6a81d75c91756e31b6c26b7746152233
|
AMapNavi-NO-IDFA: 22edfa7d6a81d75c91756e31b6c26b7746152233
|
||||||
AMapNavIOSSDK: 092382d55290f43b282ffcc522c274996794e2bc
|
AMapNavIOSSDK: e06adcb48ac8abeace46ea31f72191564e54a186
|
||||||
AMapSearch-NO-IDFA: 53b2193244be8f07f3be0a4d5161200236960587
|
AMapSearch-NO-IDFA: 53b2193244be8f07f3be0a4d5161200236960587
|
||||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
||||||
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||||
flutter_pdfview: 2e4d13ffb774858562ffbdfdb61b40744b191adc
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd
|
flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf
|
||||||
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
|
Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
|
||||||
MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406
|
MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406
|
||||||
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
|
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
|
||||||
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
|
|
||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
|
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
|
||||||
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
|
|
||||||
PODFILE CHECKSUM: b4931d4490f04261e0fda802d44e275ab3619244
|
PODFILE CHECKSUM: b4931d4490f04261e0fda802d44e275ab3619244
|
||||||
|
|
||||||
|
|||||||
@@ -508,8 +508,6 @@
|
|||||||
"-framework",
|
"-framework",
|
||||||
"\"image_picker_ios\"",
|
"\"image_picker_ios\"",
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"mobile_scanner\"",
|
|
||||||
"-framework",
|
|
||||||
"\"package_info_plus\"",
|
"\"package_info_plus\"",
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"permission_handler_apple\"",
|
"\"permission_handler_apple\"",
|
||||||
@@ -545,8 +543,6 @@
|
|||||||
"-framework",
|
"-framework",
|
||||||
"\"image_picker_ios\"",
|
"\"image_picker_ios\"",
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"mobile_scanner\"",
|
|
||||||
"-framework",
|
|
||||||
"\"package_info_plus\"",
|
"\"package_info_plus\"",
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"permission_handler_apple\"",
|
"\"permission_handler_apple\"",
|
||||||
@@ -772,8 +768,6 @@
|
|||||||
"-framework",
|
"-framework",
|
||||||
"\"image_picker_ios\"",
|
"\"image_picker_ios\"",
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"mobile_scanner\"",
|
|
||||||
"-framework",
|
|
||||||
"\"package_info_plus\"",
|
"\"package_info_plus\"",
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"permission_handler_apple\"",
|
"\"permission_handler_apple\"",
|
||||||
@@ -809,8 +803,6 @@
|
|||||||
"-framework",
|
"-framework",
|
||||||
"\"image_picker_ios\"",
|
"\"image_picker_ios\"",
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"mobile_scanner\"",
|
|
||||||
"-framework",
|
|
||||||
"\"package_info_plus\"",
|
"\"package_info_plus\"",
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"permission_handler_apple\"",
|
"\"permission_handler_apple\"",
|
||||||
@@ -873,8 +865,6 @@
|
|||||||
"-framework",
|
"-framework",
|
||||||
"\"image_picker_ios\"",
|
"\"image_picker_ios\"",
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"mobile_scanner\"",
|
|
||||||
"-framework",
|
|
||||||
"\"package_info_plus\"",
|
"\"package_info_plus\"",
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"permission_handler_apple\"",
|
"\"permission_handler_apple\"",
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>需要访问您的相机以扫描二维码</string>
|
<string>需要访问您的相机以扫描二维码</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
@@ -57,6 +59,8 @@
|
|||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>location</string>
|
<string>location</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIFileSharingEnabled</key>
|
||||||
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
|
|||||||
22
ln_jq_app/lib/common/AuthGuard.dart
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,9 @@ class StationModel {
|
|||||||
final String address;
|
final String address;
|
||||||
final String price;
|
final String price;
|
||||||
final String siteStatusName; // 例如 "维修中"
|
final String siteStatusName; // 例如 "维修中"
|
||||||
final int isSelect; // 新增字段 1是可用 0是不可用
|
final int isSelect; // 1是可用 0是不可用
|
||||||
|
final String startBusiness; // 新增:可预约最早开始时间,如 "06:00:00"
|
||||||
|
final String endBusiness; // 新增:可预约最晚结束时间,如 "22:00:00"
|
||||||
|
|
||||||
StationModel({
|
StationModel({
|
||||||
required this.hydrogenId,
|
required this.hydrogenId,
|
||||||
@@ -13,9 +15,10 @@ class StationModel {
|
|||||||
required this.price,
|
required this.price,
|
||||||
required this.siteStatusName,
|
required this.siteStatusName,
|
||||||
required this.isSelect,
|
required this.isSelect,
|
||||||
|
required this.startBusiness,
|
||||||
|
required this.endBusiness,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 从 JSON map 创建对象的工厂构造函数
|
|
||||||
factory StationModel.fromJson(Map<String, dynamic> json) {
|
factory StationModel.fromJson(Map<String, dynamic> json) {
|
||||||
return StationModel(
|
return StationModel(
|
||||||
hydrogenId: json['hydrogenId'] ?? '',
|
hydrogenId: json['hydrogenId'] ?? '',
|
||||||
@@ -23,7 +26,9 @@ class StationModel {
|
|||||||
address: json['address'] ?? '地址未知',
|
address: json['address'] ?? '地址未知',
|
||||||
price: json['price']?.toString() ?? '0.00',
|
price: json['price']?.toString() ?? '0.00',
|
||||||
siteStatusName: json['siteStatusName'] ?? '',
|
siteStatusName: json['siteStatusName'] ?? '',
|
||||||
isSelect: json['isSelect'] as int? ?? 0, // 新增字段的解析,默认为 0
|
isSelect: json['isSelect'] as int? ?? 0,
|
||||||
|
startBusiness: json['startBusiness'] ?? '00:00:00', // 默认全天
|
||||||
|
endBusiness: json['endBusiness'] ?? '23:59:59', // 默认全天
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class AppTheme {
|
|||||||
static const Color themeColor = Color(0xFF017137);
|
static const Color themeColor = Color(0xFF017137);
|
||||||
|
|
||||||
//是否开放域名切换
|
//是否开放域名切换
|
||||||
static const bool is_show_host = false;
|
static const bool is_show_host = true;
|
||||||
|
|
||||||
//http://192.168.110.222:8080/
|
//http://192.168.110.222:8080/
|
||||||
//http://192.168.110.44:8080/
|
//http://192.168.110.44:8080/
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class TokenInterceptor extends Interceptor {
|
|||||||
if (!options.headers.containsKey(tokenKey)) {
|
if (!options.headers.containsKey(tokenKey)) {
|
||||||
// 使用我们自定义的 key 添加 token
|
// 使用我们自定义的 key 添加 token
|
||||||
options.headers[tokenKey] = token;
|
options.headers[tokenKey] = token;
|
||||||
print("head.token: $token");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
|||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.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/model/base_model.dart';
|
||||||
import 'package:ln_jq_app/common/token_interceptor.dart';
|
import 'package:ln_jq_app/common/token_interceptor.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
@@ -65,6 +66,7 @@ void main() async {
|
|||||||
void initHttpSet() {
|
void initHttpSet() {
|
||||||
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
|
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
|
||||||
|
|
||||||
|
HttpService.to.init(timeout: 15);
|
||||||
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
||||||
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
|
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
|
||||||
HttpService.to.setOnResponseHandler((response) async {
|
HttpService.to.setOnResponseHandler((response) async {
|
||||||
@@ -76,8 +78,7 @@ void initHttpSet() {
|
|||||||
if (baseModel.code == 0 || baseModel.code == 200) {
|
if (baseModel.code == 0 || baseModel.code == 200) {
|
||||||
return null;
|
return null;
|
||||||
} else if (baseModel.code == 401) {
|
} else if (baseModel.code == 401) {
|
||||||
await StorageService.to.clearLoginInfo();
|
await AuthGuard.handle401(baseModel.message);
|
||||||
Get.offAll(() => const LoginPage());
|
|
||||||
return baseModel.message;
|
return baseModel.message;
|
||||||
} else {
|
} else {
|
||||||
return (baseModel.error.toString()).isEmpty
|
return (baseModel.error.toString()).isEmpty
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
|
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
|
||||||
|
|
||||||
class HistoryController extends GetxController {
|
class HistoryController extends GetxController with BaseControllerMixin {
|
||||||
|
@override
|
||||||
|
String get builderId => 'history';
|
||||||
|
|
||||||
// --- 定义 API 需要的日期格式化器 ---
|
// --- 定义 API 需要的日期格式化器 ---
|
||||||
final DateFormat _apiDateFormat = DateFormat('yyyy-MM-dd');
|
final DateFormat _apiDateFormat = DateFormat('yyyy-MM-dd');
|
||||||
|
|
||||||
@@ -13,8 +14,8 @@ class HistoryController extends GetxController {
|
|||||||
final Rx<DateTime> endDate = DateTime.now().obs;
|
final Rx<DateTime> endDate = DateTime.now().obs;
|
||||||
final TextEditingController plateNumberController = TextEditingController();
|
final TextEditingController plateNumberController = TextEditingController();
|
||||||
|
|
||||||
final RxString totalHydrogen = '0 kg'.obs;
|
final RxString totalHydrogen = '0'.obs;
|
||||||
final RxString totalCompletions = '0 次'.obs;
|
final RxString totalCompletions = '0'.obs;
|
||||||
|
|
||||||
final RxList<ReservationModel> historyList = <ReservationModel>[].obs;
|
final RxList<ReservationModel> historyList = <ReservationModel>[].obs;
|
||||||
final RxBool isLoading = true.obs;
|
final RxBool isLoading = true.obs;
|
||||||
@@ -23,14 +24,31 @@ class HistoryController extends GetxController {
|
|||||||
String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value);
|
String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value);
|
||||||
|
|
||||||
String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value);
|
String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value);
|
||||||
|
|
||||||
String stationName = "";
|
String stationName = "";
|
||||||
|
|
||||||
|
final Map<String, String> statusOptions = {
|
||||||
|
'': '全部',
|
||||||
|
'100': '未预约加氢',
|
||||||
|
'0': '待加氢',
|
||||||
|
'1': '已加氢',
|
||||||
|
'2': '未加氢',
|
||||||
|
'5': '拒绝加氢',
|
||||||
|
};
|
||||||
|
|
||||||
|
final RxString selectedStatus = ''.obs;
|
||||||
|
final RxString selectedDateType = ''.obs; // week, month, three_month
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
final args = Get.arguments as Map<String, dynamic>;
|
final args = Get.arguments as Map<String, dynamic>;
|
||||||
stationName = args['stationName'] as String;
|
stationName = args['stationName'] as String? ?? "";
|
||||||
|
refreshData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshData() {
|
||||||
|
getAllOrderCounts();
|
||||||
fetchHistoryData();
|
fetchHistoryData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,51 +56,50 @@ class HistoryController extends GetxController {
|
|||||||
var response = await HttpService.to.post(
|
var response = await HttpService.to.post(
|
||||||
"appointment/orderAddHyd/getAllOrderCounts",
|
"appointment/orderAddHyd/getAllOrderCounts",
|
||||||
data: {
|
data: {
|
||||||
// --- 直接使用 DateFormat 来格式化日期 ---
|
/*'startTime': _apiDateFormat.format(startDate.value),
|
||||||
'startTime': _apiDateFormat.format(startDate.value),
|
'endTime': _apiDateFormat.format(endDate.value),*/
|
||||||
'endTime': _apiDateFormat.format(endDate.value),
|
|
||||||
'plateNumber': plateNumberController.text,
|
'plateNumber': plateNumberController.text,
|
||||||
'stationName': stationName, // 加氢站名称
|
'stationName': stationName,
|
||||||
|
"status": selectedStatus.value,
|
||||||
|
"dateType": selectedDateType.value,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (response == null || response.data == null) {
|
if (response == null || response.data == null) {
|
||||||
totalHydrogen.value = '0 kg';
|
totalHydrogen.value = '0';
|
||||||
totalCompletions.value = '0 次';
|
totalCompletions.value = '0';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final baseModel = BaseModel<dynamic>.fromJson(response.data);
|
final baseModel = BaseModel<dynamic>.fromJson(response.data);
|
||||||
final dataMap = baseModel.data as Map<String, dynamic>;
|
final dataMap = baseModel.data as Map<String, dynamic>;
|
||||||
totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0} kg';
|
totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0}';
|
||||||
totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0} 次';
|
totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0}';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
totalHydrogen.value = '0 kg';
|
totalHydrogen.value = '0';
|
||||||
totalCompletions.value = '0 次';
|
totalCompletions.value = '0';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchHistoryData() async {
|
Future<void> fetchHistoryData() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
updateUi();
|
||||||
//获取数据
|
|
||||||
getAllOrderCounts();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var response = await HttpService.to.post(
|
var response = await HttpService.to.post(
|
||||||
"appointment/orderAddHyd/sitOrderPage",
|
"appointment/orderAddHyd/sitOrderPage",
|
||||||
data: {
|
data: {
|
||||||
// --- 直接使用 DateFormat 来格式化日期 ---
|
/*'startTime': _apiDateFormat.format(startDate.value),
|
||||||
'startTime': _apiDateFormat.format(startDate.value),
|
'endTime': _apiDateFormat.format(endDate.value),*/
|
||||||
'endTime': _apiDateFormat.format(endDate.value),
|
|
||||||
'plateNumber': plateNumberController.text,
|
'plateNumber': plateNumberController.text,
|
||||||
'pageNum': 1,
|
'pageNum': 1,
|
||||||
'pageSize': 50,
|
'pageSize': 50,
|
||||||
'stationName': stationName, // 加氢站名称
|
'stationName': stationName,
|
||||||
|
"status": selectedStatus.value,
|
||||||
|
"dateType": selectedDateType.value,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response == null || response.data == null) {
|
if (response == null || response.data == null) {
|
||||||
showToast('无法获取历史记录');
|
|
||||||
_resetData();
|
_resetData();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -90,7 +107,6 @@ class HistoryController extends GetxController {
|
|||||||
final baseModel = BaseModel<dynamic>.fromJson(response.data);
|
final baseModel = BaseModel<dynamic>.fromJson(response.data);
|
||||||
if (baseModel.code == 0 && baseModel.data != null) {
|
if (baseModel.code == 0 && baseModel.data != null) {
|
||||||
final dataMap = baseModel.data as Map<String, dynamic>;
|
final dataMap = baseModel.data as Map<String, dynamic>;
|
||||||
|
|
||||||
final List<dynamic> listFromServer = dataMap['records'] ?? [];
|
final List<dynamic> listFromServer = dataMap['records'] ?? [];
|
||||||
historyList.assignAll(
|
historyList.assignAll(
|
||||||
listFromServer
|
listFromServer
|
||||||
@@ -99,14 +115,13 @@ class HistoryController extends GetxController {
|
|||||||
);
|
);
|
||||||
hasData.value = historyList.isNotEmpty;
|
hasData.value = historyList.isNotEmpty;
|
||||||
} else {
|
} else {
|
||||||
showToast(baseModel.message);
|
|
||||||
_resetData();
|
_resetData();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('获取历史记录失败: $e');
|
|
||||||
_resetData();
|
_resetData();
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
updateUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,97 +130,15 @@ class HistoryController extends GetxController {
|
|||||||
hasData.value = false;
|
hasData.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void pickDate(BuildContext context, bool isStartDate) {
|
void onStatusSelected(String status) {
|
||||||
// 确定当前操作的日期和临时存储变量
|
if (selectedStatus.value == status) return;
|
||||||
final DateTime initialDate = isStartDate ? startDate.value : endDate.value;
|
selectedStatus.value = status;
|
||||||
DateTime tempDate = initialDate;
|
refreshData();
|
||||||
|
|
||||||
// 定义全局的最早可选日期
|
|
||||||
final DateTime globalMinimumDate = DateTime(2025, 12, 1);
|
|
||||||
|
|
||||||
// 动态计算当前选择器的最小/最大日期范围
|
|
||||||
DateTime minimumDate;
|
|
||||||
DateTime? maximumDate; // 声明为可空,因为两个日期都可能没有最大限制
|
|
||||||
|
|
||||||
if (isStartDate) {
|
|
||||||
// 当选择【开始日期】时 它的最小日期就是全局最小日期
|
|
||||||
minimumDate = globalMinimumDate;
|
|
||||||
// 最大日期没有限制
|
|
||||||
maximumDate = null;
|
|
||||||
} else {
|
|
||||||
// 当选择【结束日期】时 它的最小日期不能早于当前的开始日期
|
|
||||||
minimumDate = startDate.value;
|
|
||||||
// 确认结束日期没有最大限制 ---
|
|
||||||
//最大日期没有限制
|
|
||||||
maximumDate = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Get.bottomSheet(
|
void onDateTypeSelected(String type) {
|
||||||
Container(
|
selectedDateType.value = type;
|
||||||
height: 300,
|
refreshData();
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// 顶部的取消和确认按钮
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
child: const Text('取消', style: TextStyle(color: Colors.grey)),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
// 4. 确认后,更新对应的日期变量
|
|
||||||
if (isStartDate) {
|
|
||||||
startDate.value = tempDate;
|
|
||||||
// 如果新的开始日期晚于结束日期,自动将结束日期调整为同一天
|
|
||||||
if (tempDate.isAfter(endDate.value)) {
|
|
||||||
endDate.value = tempDate;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
endDate.value = tempDate;
|
|
||||||
}
|
|
||||||
Get.back();
|
|
||||||
|
|
||||||
// 选择日期后自动刷新数据
|
|
||||||
fetchHistoryData();
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'确认',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
// 日期选择器
|
|
||||||
Expanded(
|
|
||||||
child: CupertinoDatePicker(
|
|
||||||
mode: CupertinoDatePickerMode.date,
|
|
||||||
initialDateTime: initialDate,
|
|
||||||
// 应用动态计算好的最小/最大日期
|
|
||||||
minimumDate: minimumDate,
|
|
||||||
maximumDate: maximumDate,
|
|
||||||
onDateTimeChanged: (DateTime newDate) {
|
|
||||||
tempDate = newDate;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.transparent, // 使底部工作表外的区域透明
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,252 +1,313 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/pages/b_page/history/controller.dart';
|
import 'package:ln_jq_app/pages/b_page/history/controller.dart';
|
||||||
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
|
import 'package:ln_jq_app/pages/b_page/site/controller.dart';
|
||||||
|
|
||||||
class HistoryPage extends GetView<HistoryController> {
|
class HistoryPage extends GetView<HistoryController> {
|
||||||
const HistoryPage({Key? key}) : super(key: key);
|
const HistoryPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(HistoryController());
|
return GetBuilder<HistoryController>(
|
||||||
|
init: HistoryController(),
|
||||||
|
id: 'history',
|
||||||
|
builder: (_) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('历史记录'), centerTitle: true),
|
backgroundColor: const Color(0xFFF7F8FA),
|
||||||
body: Padding(
|
appBar: AppBar(
|
||||||
padding: const EdgeInsets.all(12.0),
|
backgroundColor: Colors.white,
|
||||||
child: Column(
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20),
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
),
|
||||||
|
title: _buildSearchBox(),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildFilterCard(context),
|
_buildFilterBar(),
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildSummaryCard(),
|
_buildSummaryCard(),
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildListHeader(),
|
|
||||||
Expanded(child: _buildHistoryList()),
|
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 _buildFilterCard(BuildContext context) {
|
Widget _buildFilterBar() {
|
||||||
return Card(
|
return Container(
|
||||||
elevation: 2,
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Padding(
|
child: Row(
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
const Text('时间范围', style: TextStyle(fontSize: 14, color: Colors.grey)),
|
Expanded(
|
||||||
const SizedBox(height: 8),
|
child: SingleChildScrollView(
|
||||||
Row(
|
scrollDirection: Axis.horizontal,
|
||||||
children: [
|
child: Row(
|
||||||
Expanded(child: _buildDateField(context, true)),
|
children: controller.statusOptions.entries.map((entry) {
|
||||||
const Padding(
|
return Obx(() {
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
bool isSelected = controller.selectedStatus.value == entry.key;
|
||||||
child: Text('至'),
|
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),
|
||||||
),
|
),
|
||||||
Expanded(child: _buildDateField(context, false)),
|
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(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
);
|
||||||
const Text('车牌号', style: TextStyle(fontSize: 14, color: Colors.grey)),
|
}
|
||||||
const SizedBox(height: 8),
|
|
||||||
SizedBox(
|
Widget _buildTimeFilterIcon() {
|
||||||
height: 44,
|
return PopupMenuButton<String>(
|
||||||
child: TextField(
|
icon: LoginUtil.getAssImg("ic_ex_menu@2x"),
|
||||||
controller: controller.plateNumberController,
|
onSelected: controller.onDateTypeSelected,
|
||||||
decoration: InputDecoration(
|
itemBuilder: (context) => [
|
||||||
hintText: '请输入车牌号',
|
const PopupMenuItem(value: 'week', child: Text('最近一周')),
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
const PopupMenuItem(value: 'month', child: Text('最近一月')),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
const PopupMenuItem(value: 'three_month', child: Text('最近三月')),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
FocusScope.of(context).unfocus(); // Hide keyboard
|
|
||||||
controller.fetchHistoryData();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.search, size: 20),
|
|
||||||
label: const Text('查询'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
minimumSize: const Size(double.infinity, 44),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSummaryCard() {
|
Widget _buildSummaryCard() {
|
||||||
return Card(
|
return Container(
|
||||||
elevation: 2,
|
margin: const EdgeInsets.only(left: 16, right: 16,bottom: 12),
|
||||||
child: Padding(
|
padding: const EdgeInsets.all(20),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 20.0),
|
height: 160,
|
||||||
child: Obx(
|
width: double.infinity,
|
||||||
() => Row(
|
decoration: BoxDecoration(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
borderRadius: BorderRadius.circular(24),
|
||||||
children: [
|
image: const DecorationImage(
|
||||||
_buildSummaryItem('实际加氢总量', controller.totalHydrogen.value, Colors.blue),
|
image: AssetImage('assets/images/history_bg.png'),
|
||||||
const SizedBox(width: 1, height: 40, child: VerticalDivider()),
|
fit: BoxFit.cover,
|
||||||
_buildSummaryItem(
|
|
||||||
'预约完成次数',
|
|
||||||
controller.totalCompletions.value,
|
|
||||||
Colors.green,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
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() {
|
Widget _buildHistoryList() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (controller.isLoading.value) {
|
if (controller.isLoading.value) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
if (!controller.hasData.value) {
|
if (controller.historyList.isEmpty) {
|
||||||
return const Center(child: Text('没有找到相关记录'));
|
return const Center(
|
||||||
|
child: Text('暂无相关记录', style: TextStyle(color: Color(0xFF999999))),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
itemCount: controller.historyList.length,
|
itemCount: controller.historyList.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final ReservationModel item = controller.historyList[index];
|
return _buildHistoryItem(controller.historyList[index]);
|
||||||
return Card(
|
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
|
||||||
child: ListTile(
|
|
||||||
title: Text('车牌号: ${item.plateNumber}'),
|
|
||||||
subtitle: Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: '加氢站: ${item.stationName}\n',
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: '时间: ${item.time}\n',
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: '加氢量:',
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: '${item.amount}',
|
|
||||||
style: TextStyle(fontSize: 16, color: AppTheme.themeColor),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
,
|
|
||||||
trailing:
|
|
||||||
// 状态标签
|
|
||||||
_buildStatusChip(item.status),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatusChip(ReservationStatus status) {
|
Widget _buildHistoryItem(ReservationModel item) {
|
||||||
String text;
|
return Container(
|
||||||
Color color;
|
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) {
|
switch (status) {
|
||||||
case ReservationStatus.pending:
|
case ReservationStatus.pending:
|
||||||
text = '待加氢';
|
text = '待加氢';
|
||||||
color = Colors.orange;
|
bgColor = const Color(0xFFFFF7E8);
|
||||||
|
textColor = const Color(0xFFFF9800);
|
||||||
break;
|
break;
|
||||||
case ReservationStatus.completed:
|
case ReservationStatus.completed:
|
||||||
text = '已加氢';
|
text = '已加氢';
|
||||||
color = Colors.greenAccent;
|
bgColor = const Color(0xFFE8F5E9);
|
||||||
|
textColor = const Color(0xFF4CAF50);
|
||||||
break;
|
break;
|
||||||
case ReservationStatus.rejected:
|
case ReservationStatus.rejected:
|
||||||
text = '拒绝加氢';
|
text = '拒绝加氢';
|
||||||
color = Colors.red;
|
bgColor = const Color(0xFFFFEBEE);
|
||||||
|
textColor = const Color(0xFFF44336);
|
||||||
break;
|
break;
|
||||||
case ReservationStatus.unadded:
|
case ReservationStatus.unadded:
|
||||||
text = '未加氢';
|
text = '未加氢';
|
||||||
color = Colors.red;
|
bgColor = const Color(0xFFFFEBEE);
|
||||||
|
textColor = const Color(0xFFF44336);
|
||||||
break;
|
break;
|
||||||
case ReservationStatus.cancel:
|
case ReservationStatus.cancel:
|
||||||
text = '已取消';
|
text = '已取消';
|
||||||
color = Colors.red;
|
bgColor = const Color(0xFFFFEBEE);
|
||||||
|
textColor = const Color(0xFFF44336);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
text = '未知状态';
|
text = '未知状态';
|
||||||
color = Colors.grey;
|
bgColor = Colors.grey;
|
||||||
|
textColor = Colors.grey;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.1),
|
color: bgColor,
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.circle, color: color, size: 8),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDateField(BuildContext context, bool isStart) {
|
|
||||||
return Obx(
|
|
||||||
() => InkWell(
|
|
||||||
onTap: () => controller.pickDate(context, isStart),
|
|
||||||
child: Container(
|
|
||||||
height: 44,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.grey.shade400),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: textColor.withOpacity(0.3)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Text(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
text,
|
||||||
children: [
|
style: TextStyle(color: textColor, fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
Text(isStart ? controller.formattedStartDate : controller.formattedEndDate),
|
|
||||||
const Icon(Icons.calendar_today, size: 18, color: Colors.grey),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSummaryItem(String label, String value, Color color) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: TextStyle(color: color, fontSize: 22, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildListHeader() {
|
|
||||||
return const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 14.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text('加氢明细', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/common/login_util.dart';
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/pages/b_page/reservation/controller.dart';
|
import 'package:ln_jq_app/pages/b_page/reservation/controller.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
||||||
|
import 'package:ln_jq_app/pages/common/webview/view.dart';
|
||||||
|
|
||||||
class ReservationPage extends GetView<ReservationController> {
|
class ReservationPage extends GetView<ReservationController> {
|
||||||
const ReservationPage({super.key});
|
const ReservationPage({super.key});
|
||||||
@@ -36,7 +40,31 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
_buildSystemTips(),
|
_buildSystemTips(),
|
||||||
SizedBox(height: 24),
|
SizedBox(height: 24),
|
||||||
_buildLogoutButton(),
|
_buildLogoutButton(),
|
||||||
SizedBox(height: 75.h),
|
SizedBox(height: 15.h),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: '《用户协议》',
|
||||||
|
style: TextStyle(color: Colors.blue, fontSize: 13),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
openPage("用户协议", "https://lnh2e.com/user_agreement.html");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: '《隐私政策》',
|
||||||
|
style: TextStyle(color: Colors.blue, fontSize: 13),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 95.h),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -48,6 +76,14 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void openPage(String title, String url) {
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
openWebPage(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Get.to(() => const WebViewPage(), arguments: {'title': title, 'url': url});
|
||||||
|
}
|
||||||
|
|
||||||
/// 1. 顶部个人信息及统计栏
|
/// 1. 顶部个人信息及统计栏
|
||||||
Widget _buildTopSection(BuildContext context) {
|
Widget _buildTopSection(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -78,12 +114,16 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Flexible(
|
||||||
|
child: Text(
|
||||||
controller.name,
|
controller.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildStatusTag(),
|
_buildStatusTag(),
|
||||||
@@ -98,12 +138,11 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async{
|
onPressed: () async {
|
||||||
var scanResult = await Get.to(() => const MessagePage());
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
if (scanResult == null) {
|
if (scanResult == null) {
|
||||||
controller.msgNotice();
|
controller.msgNotice();
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: Colors.grey[100],
|
||||||
@@ -111,9 +150,7 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
),
|
),
|
||||||
icon: Badge(
|
icon: Badge(
|
||||||
smallSize: 8,
|
smallSize: 8,
|
||||||
backgroundColor: controller.isNotice
|
backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
|
||||||
? Colors.red
|
|
||||||
: Colors.transparent,
|
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.notifications_outlined,
|
Icons.notifications_outlined,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
@@ -232,14 +269,19 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 11.sp),
|
style: TextStyle(color: Colors.grey, fontSize: 11.sp),
|
||||||
),
|
),
|
||||||
Text(
|
Expanded(
|
||||||
|
child: Text(
|
||||||
value,
|
value,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF333333),
|
color: const Color(0xFF333333),
|
||||||
fontSize: 12.sp,
|
fontSize: 12.sp,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -515,14 +557,15 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: kPrimaryColor,
|
backgroundColor: kPrimaryColor,
|
||||||
minimumSize: const Size(double.infinity, 50),
|
minimumSize: const Size(double.infinity, 50),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Text("发送广播", style: TextStyle(color: Colors.white)),
|
child: const Text("发送广播", style: TextStyle(color: Colors.white)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/common/login_util.dart';
|
import 'package:ln_jq_app/common/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/b_page/history/view.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
||||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
@@ -57,17 +56,33 @@ class SitePage extends GetView<SiteController> {
|
|||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.to(
|
// 手动录入
|
||||||
() => HistoryPage(),
|
controller.confirmReservation("", isAdd: true);
|
||||||
arguments: {'stationName': controller.name},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Container(
|
||||||
'历史记录',
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: const Color(0xFFEEEEEE)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.add_circle_outline,
|
||||||
|
size: 18,
|
||||||
|
color: Color(0xFF666666),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
"无预约车辆加氢",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.sp,
|
color: Color.fromRGBO(51, 51, 51, 1),
|
||||||
fontWeight: FontWeight.bold,
|
fontSize: 13.sp,
|
||||||
color: Color.fromRGBO(156, 163, 175, 1),
|
fontWeight: FontWeight.w400
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -78,12 +93,12 @@ class SitePage extends GetView<SiteController> {
|
|||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
_buildSearchView(),
|
_buildSearchView(),
|
||||||
|
SizedBox(height: 15.h),
|
||||||
controller.hasReservationData
|
controller.hasReservationData
|
||||||
? _buildReservationListView()
|
? _buildReservationListView()
|
||||||
: _buildEmptyReservationView(),
|
: _buildEmptyReservationView(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 35.h),
|
SizedBox(height: 35.h),
|
||||||
//第三部分
|
//第三部分
|
||||||
Container(
|
Container(
|
||||||
@@ -136,7 +151,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 75.h),
|
SizedBox(height: 105.h),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -185,27 +200,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
_buildDropdownMenu(),
|
||||||
onPressed: () async {
|
|
||||||
var scanResult = await Get.to(() => const MessagePage());
|
|
||||||
if (scanResult == null) {
|
|
||||||
controller.msgNotice();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style: IconButton.styleFrom(
|
|
||||||
backgroundColor: Colors.grey[100],
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
),
|
|
||||||
icon: Badge(
|
|
||||||
smallSize: 8,
|
|
||||||
backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.notifications_outlined,
|
|
||||||
color: Colors.black87,
|
|
||||||
size: 30,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 25),
|
const SizedBox(height: 25),
|
||||||
@@ -233,6 +228,40 @@ class SitePage extends GetView<SiteController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDropdownMenu() {
|
||||||
|
return PopupMenuButton<String>(
|
||||||
|
icon: Container(child: const Icon(Icons.grid_view_rounded, size: 24)),
|
||||||
|
onSelected: (value) async {
|
||||||
|
if (value == 'message') {
|
||||||
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
|
if (scanResult == null) {
|
||||||
|
controller.msgNotice();
|
||||||
|
}
|
||||||
|
} else if (value == 'history') {
|
||||||
|
Get.to(() => const HistoryPage(), arguments: {'stationName': controller.name});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: 'message',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.notifications_none, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('消息中心'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: 'history',
|
||||||
|
child: Row(
|
||||||
|
children: [Icon(Icons.history, size: 20), SizedBox(width: 8), Text('加氢历史')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
|
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -391,8 +420,9 @@ class SitePage extends GetView<SiteController> {
|
|||||||
|
|
||||||
/// 构建“有预约数据”的列表视图
|
/// 构建“有预约数据”的列表视图
|
||||||
Widget _buildReservationListView() {
|
Widget _buildReservationListView() {
|
||||||
return ListView.separated(
|
return ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
// 因为外层已有滚动,这里禁用内部滚动
|
// 因为外层已有滚动,这里禁用内部滚动
|
||||||
itemCount: controller.reservationList.length,
|
itemCount: controller.reservationList.length,
|
||||||
@@ -401,7 +431,6 @@ class SitePage extends GetView<SiteController> {
|
|||||||
// 调用新的方法来构建每一项
|
// 调用新的方法来构建每一项
|
||||||
return _buildReservationItem(index, item);
|
return _buildReservationItem(index, item);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const SizedBox(height: 0), // 列表项之间的间距
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +495,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
/// 右侧具体数据卡片
|
/// 右侧具体数据卡片
|
||||||
Widget _buildInfoCard(ReservationModel item) {
|
Widget _buildInfoCard(ReservationModel item) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.only(left: 16.w, top: 8.5, bottom: 8.5, right: 16.w),
|
padding: EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
@@ -508,22 +537,42 @@ class SitePage extends GetView<SiteController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// 联系信息
|
// 联系信息
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
Text(
|
||||||
"${item.contactPerson} | ${item.contactPhone}",
|
item.contactPerson.isEmpty || item.contactPhone.isEmpty
|
||||||
|
? ""
|
||||||
|
: "${item.contactPerson} | ${item.contactPhone}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF999999),
|
color: Color(0xFF999999),
|
||||||
fontSize: 13.sp,
|
fontSize: 13.sp,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 6.h),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (item.hasDrivingAttachment)
|
||||||
|
controller.buildInfoTag('行驶证',item.drivingAttachments),
|
||||||
|
if (item.hasHydrogenationAttachment) ...[
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
controller.buildInfoTag('加氢证',item.hydrogenationAttachments)
|
||||||
],
|
],
|
||||||
|
Spacer(),
|
||||||
|
if (item.isEdit == "1") ...[
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
const Divider(height: 1, color: Color(0xFFF5F5F5)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: _buildSmallButton(
|
||||||
|
"修改信息",
|
||||||
|
isOutline: true,
|
||||||
|
onTap: () {
|
||||||
|
controller.confirmReservation(item.id, isEdit: true);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
//操作按钮(仅在待处理状态显示)
|
] else if (item.status == ReservationStatus.pending) ...[
|
||||||
if (item.status == ReservationStatus.pending) ...[
|
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
const Divider(height: 1, color: Color(0xFFF5F5F5)),
|
const Divider(height: 1, color: Color(0xFFF5F5F5)),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -550,17 +599,20 @@ class SitePage extends GetView<SiteController> {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通用小按钮
|
|
||||||
|
|
||||||
Widget _buildSmallButton(
|
Widget _buildSmallButton(
|
||||||
String text, {
|
String text, {
|
||||||
required bool isOutline,
|
required bool isOutline,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) {
|
}) {
|
||||||
const kPrimaryGreen = Color(0xFF006D35);
|
const kPrimaryGreen = Color(0xFF006D35);
|
||||||
const kDangerRed = Color(0xFFFF7D7D);
|
var kDangerRed = text.contains('修改') ? Colors.red : Color.fromRGBO(255, 142, 98, 1);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -634,139 +686,4 @@ class SitePage extends GetView<SiteController> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 右侧操作按钮(拒绝/确认)
|
|
||||||
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 = '待加氢';
|
|
||||||
color = Colors.orange;
|
|
||||||
break;
|
|
||||||
case ReservationStatus.completed:
|
|
||||||
text = '已加氢';
|
|
||||||
color = Colors.greenAccent;
|
|
||||||
break;
|
|
||||||
case ReservationStatus.rejected:
|
|
||||||
text = '拒绝加氢';
|
|
||||||
color = Colors.red;
|
|
||||||
break;
|
|
||||||
case ReservationStatus.unadded:
|
|
||||||
text = '未加氢';
|
|
||||||
color = Colors.red;
|
|
||||||
break;
|
|
||||||
case ReservationStatus.cancel:
|
|
||||||
text = '已取消';
|
|
||||||
color = Colors.red;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
text = '未知状态';
|
|
||||||
color = Colors.grey;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.circle, color: color, size: 8),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建信息详情行
|
|
||||||
Widget _buildDetailRow(
|
|
||||||
IconData icon,
|
|
||||||
String label,
|
|
||||||
String value, {
|
|
||||||
Color valueColor = Colors.black87,
|
|
||||||
}) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: Colors.grey, size: 20),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text('$label: ', style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: valueColor,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 底部构建带图标的提示信息行
|
|
||||||
Widget _buildInfoItem(IconData icon, String text) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: Colors.blue, size: 20),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(text, style: const TextStyle(fontSize: 14, color: Colors.black54)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,45 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:getx_scaffold/common/common.dart';
|
||||||
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
|
||||||
/// 原生地图页面
|
/// 原生地图页面
|
||||||
class NativePageIOS extends StatelessWidget {
|
class NativePageIOS extends StatefulWidget {
|
||||||
const NativePageIOS({super.key});
|
const NativePageIOS({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NativePageIOS> createState() => _NativePageIOSState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NativePageIOSState extends State<NativePageIOS> {
|
||||||
|
bool _androidPermissionReady = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
requestAndroidPermission();
|
||||||
|
} else {
|
||||||
|
// iOS 已经在原生端处理好,直接置为 true
|
||||||
|
_androidPermissionReady = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
return _buildIOSView(context);
|
return _buildIOSView(context);
|
||||||
} else if (Platform.isAndroid) {
|
} else if (Platform.isAndroid) {
|
||||||
|
// 如果安卓权限还没处理完,显示加载中,不加载原生 View
|
||||||
|
if (!_androidPermissionReady) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(color: Color(0xFF017137)),
|
||||||
|
);
|
||||||
|
}
|
||||||
return _buildAndroidView(context);
|
return _buildAndroidView(context);
|
||||||
} else {
|
} else {
|
||||||
return const Center(
|
return const Center(child: Text('不支持的平台', style: TextStyle(fontSize: 16)));
|
||||||
child: Text('不支持的平台', style: TextStyle(fontSize: 16)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +54,8 @@ class NativePageIOS extends StatelessWidget {
|
|||||||
height: MediaQuery.of(context).size.height - 100,
|
height: MediaQuery.of(context).size.height - 100,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
child: UiKitView(
|
child: UiKitView(
|
||||||
viewType: 'NativeFirstPage', // 与iOS原生端注册的标识一致
|
viewType: 'NativeFirstPage',
|
||||||
|
// 与iOS原生端注册的标识一致
|
||||||
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{}.toSet(),
|
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{}.toSet(),
|
||||||
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
|
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
|
||||||
creationParamsCodec: const StandardMessageCodec(),
|
creationParamsCodec: const StandardMessageCodec(),
|
||||||
@@ -60,7 +85,8 @@ class NativePageIOS extends StatelessWidget {
|
|||||||
id: params.id,
|
id: params.id,
|
||||||
viewType: 'NativeFirstPage',
|
viewType: 'NativeFirstPage',
|
||||||
layoutDirection: TextDirection.ltr,
|
layoutDirection: TextDirection.ltr,
|
||||||
creationParams: {}, // 你的参数
|
creationParams: {},
|
||||||
|
// 你的参数
|
||||||
creationParamsCodec: const StandardMessageCodec(),
|
creationParamsCodec: const StandardMessageCodec(),
|
||||||
onFocus: () {
|
onFocus: () {
|
||||||
params.onFocusChanged(true);
|
params.onFocusChanged(true);
|
||||||
@@ -73,28 +99,31 @@ class NativePageIOS extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理点击事件(如需要)
|
void requestAndroidPermission() async {
|
||||||
void _handleTap(BuildContext context) {
|
try {
|
||||||
if (kDebugMode) {
|
final deviceInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
print("NativePage被点击");
|
final sdkInt = deviceInfo.version.sdkInt;
|
||||||
}
|
|
||||||
_showDialog(context, '提示', '点击了原生地图页面');
|
List<Permission> permissions = [Permission.location];
|
||||||
|
|
||||||
|
if (sdkInt < 33) {
|
||||||
|
permissions.add(Permission.storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示对话框
|
// 发起请求
|
||||||
void _showDialog(BuildContext context, String title, String content) {
|
Map<Permission, PermissionStatus> statuses = await permissions.request();
|
||||||
showDialog(
|
|
||||||
context: context,
|
if (statuses[Permission.location]?.isDenied ?? false) {
|
||||||
builder: (context) => AlertDialog(
|
Toast.show("定位权限被拒绝,会影响相关功能使用");
|
||||||
title: Text(title),
|
}
|
||||||
content: Text(content),
|
} catch (e) {
|
||||||
actions: [
|
debugPrint("权限申请异常: $e");
|
||||||
TextButton(
|
} finally {
|
||||||
onPressed: () => Navigator.pop(context),
|
if (mounted) {
|
||||||
child: const Text('确定'),
|
setState(() {
|
||||||
),
|
_androidPermissionReady = true;
|
||||||
],
|
});
|
||||||
),
|
}
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,11 @@ import 'package:getx_scaffold/getx_scaffold.dart';
|
|||||||
import 'package:ln_jq_app/common/login_util.dart';
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/base_widgets/NativePageIOS.dart';
|
import 'package:ln_jq_app/pages/c_page/base_widgets/NativePageIOS.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/car_info/view.dart';
|
import 'package:ln_jq_app/pages/c_page/car_info/view.dart';
|
||||||
|
|
||||||
import 'package:ln_jq_app/pages/c_page/mine/view.dart';
|
import 'package:ln_jq_app/pages/c_page/mine/view.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/reservation/view.dart';
|
import 'package:ln_jq_app/pages/c_page/reservation/view.dart';
|
||||||
|
|
||||||
|
import '../mall/mall_view.dart';
|
||||||
import 'index.dart';
|
import 'index.dart';
|
||||||
|
|
||||||
class BaseWidgetsPage extends GetView<BaseWidgetsController> {
|
class BaseWidgetsPage extends GetView<BaseWidgetsController> {
|
||||||
@@ -32,14 +34,14 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildPages() {
|
List<Widget> _buildPages() {
|
||||||
return [ReservationPage(), NativePageIOS(), CarInfoPage(), MinePage()];
|
return [ReservationPage(), NativePageIOS(), MallPage(), CarInfoPage(), MinePage()];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义导航栏 (悬浮胶囊样式)
|
// 自定义导航栏 (悬浮胶囊样式)
|
||||||
Widget _buildNavigationBar() {
|
Widget _buildNavigationBar() {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 50.h,
|
height: Get.height * 0.05,
|
||||||
margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
|
margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
|
color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
|
||||||
@@ -57,8 +59,9 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
|
|||||||
children: [
|
children: [
|
||||||
_buildNavItem(0, "ic_h2_select@2x", "ic_h2@2x"),
|
_buildNavItem(0, "ic_h2_select@2x", "ic_h2@2x"),
|
||||||
_buildNavItem(1, "ic_map_select@2x", "ic_map@2x"),
|
_buildNavItem(1, "ic_map_select@2x", "ic_map@2x"),
|
||||||
_buildNavItem(2, "ic_car_select@2x", "ic_car@2x"),
|
_buildNavItem(2, "ic_mall_select@2x", "ic_mall@2x"),
|
||||||
_buildNavItem(3, "ic_user_select@2x", "ic_user@2x"),
|
_buildNavItem(3, "ic_car_select@2x", "ic_car@2x"),
|
||||||
|
_buildNavItem(4, "ic_user_select@2x", "ic_user@2x"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -81,7 +84,8 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),),
|
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:getx_scaffold/getx_scaffold.dart' as dio;
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:image_picker_platform_interface/src/types/image_source.dart';
|
||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/car_info/attachment_viewer_page.dart';
|
import 'package:ln_jq_app/pages/c_page/car_info/attachment_viewer_page.dart';
|
||||||
@@ -15,9 +18,9 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
// --- 车辆基本信息 ---
|
// --- 车辆基本信息 ---
|
||||||
String plateNumber = "";
|
String plateNumber = "";
|
||||||
String vin = "未知";
|
String vin = "-";
|
||||||
String modelName = "未知";
|
String modelName = "-";
|
||||||
String brandName = "未知";
|
String brandName = "-";
|
||||||
|
|
||||||
// --- 证件附件列表 ---
|
// --- 证件附件列表 ---
|
||||||
final RxList<String> drivingAttachments = <String>[].obs;
|
final RxList<String> drivingAttachments = <String>[].obs;
|
||||||
@@ -89,6 +92,113 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//上传证件照
|
||||||
|
void pickImage(String title, ImageSource source) async {
|
||||||
|
if (source == ImageSource.camera) {
|
||||||
|
takePhotoAndRecognize(title);
|
||||||
|
} else if (source == ImageSource.gallery) {
|
||||||
|
//相册选择逻辑
|
||||||
|
var status = await Permission.photos.request();
|
||||||
|
if (!status.isGranted) {
|
||||||
|
if (status.isPermanentlyDenied) openAppSettings();
|
||||||
|
showErrorToast("需要相册权限才能拍照上传");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final XFile? image = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
imageQuality: 80,
|
||||||
|
);
|
||||||
|
if (image != null) {
|
||||||
|
_uploadAndSaveCertificate(title, image.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void takePhotoAndRecognize(String title) async {
|
||||||
|
var status = await Permission.camera.request();
|
||||||
|
if (!status.isGranted) {
|
||||||
|
if (status.isPermanentlyDenied) openAppSettings();
|
||||||
|
showErrorToast("需要相机权限才能拍照上传");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final XFile? photo = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 80, // 压缩图片质量以加快上传
|
||||||
|
);
|
||||||
|
if (photo == null) return;
|
||||||
|
|
||||||
|
_uploadAndSaveCertificate(title, photo.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提取共用的上传与关联证件逻辑
|
||||||
|
void _uploadAndSaveCertificate(String title, String filePath) async {
|
||||||
|
// 上传文件
|
||||||
|
String? imageUrl = await uploadFile(filePath);
|
||||||
|
if (imageUrl == null) return;
|
||||||
|
|
||||||
|
// 根据标题映射业务类型
|
||||||
|
String type = "";
|
||||||
|
switch (title) {
|
||||||
|
case "行驶证":
|
||||||
|
type = "9";
|
||||||
|
break;
|
||||||
|
case "营运证":
|
||||||
|
type = "10";
|
||||||
|
break;
|
||||||
|
case "加氢证":
|
||||||
|
type = "12";
|
||||||
|
break;
|
||||||
|
case "登记证":
|
||||||
|
type = "13";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用后台接口关联证件
|
||||||
|
var response = await HttpService.to.post(
|
||||||
|
"appointment/truck/uploadCertificatePic",
|
||||||
|
data: {
|
||||||
|
"plateNumber": plateNumber,
|
||||||
|
"imageUrl": imageUrl,
|
||||||
|
"type": type,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0) {
|
||||||
|
showSuccessToast("上传成功");
|
||||||
|
getUserBindCarInfo(); // 重新拉取数据更新UI
|
||||||
|
} else {
|
||||||
|
showErrorToast(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 上传图片
|
||||||
|
Future<String?> uploadFile(String filePath) async {
|
||||||
|
showLoading("正在上传...");
|
||||||
|
try {
|
||||||
|
dio.FormData formData = dio.FormData.fromMap({
|
||||||
|
'file': await dio.MultipartFile.fromFile(filePath, filename: 'ocr_identity.jpg'),
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await HttpService.to.post("appointment/ocr/upload", data: formData);
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0) return result.data.toString();
|
||||||
|
showErrorToast(result.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorToast("图片上传失败");
|
||||||
|
} finally {
|
||||||
|
dismissLoading();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
void getUserBindCarInfo() async {
|
void getUserBindCarInfo() async {
|
||||||
if (StorageService.to.hasVehicleInfo) {
|
if (StorageService.to.hasVehicleInfo) {
|
||||||
VehicleInfo? bean = StorageService.to.vehicleInfo;
|
VehicleInfo? bean = StorageService.to.vehicleInfo;
|
||||||
@@ -102,7 +212,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
// 获取证件信息
|
// 获取证件信息
|
||||||
final response = await HttpService.to.get(
|
final response = await HttpService.to.get(
|
||||||
'appointment/vehicle/getPicInfoByVin?vin=$vin',
|
'appointment/vehicle/getPicInfoByVin?vin=$vin&plateNumber=$plateNumber',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != null && response.data != null) {
|
if (response != null && response.data != null) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:ln_jq_app/common/login_util.dart';
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
@@ -133,7 +134,7 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async{
|
onPressed: () async {
|
||||||
var scanResult = await Get.to(() => const MessagePage());
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
if (scanResult == null) {
|
if (scanResult == null) {
|
||||||
controller.msgNotice();
|
controller.msgNotice();
|
||||||
@@ -163,11 +164,26 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_buildModernStatItem('本月里程数', 'Accumulated', '2,852km', ''),
|
_buildModernStatItem(
|
||||||
|
'本月里程数',
|
||||||
|
'Accumulated',
|
||||||
|
StorageService.to.hasVehicleInfo ? '2,852km' : '-',
|
||||||
|
'',
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildModernStatItem('总里程', 'Refuel Count', "2.5W km", ''),
|
_buildModernStatItem(
|
||||||
|
'总里程',
|
||||||
|
'Refuel Count',
|
||||||
|
StorageService.to.hasVehicleInfo ? "2.5W km" : '-',
|
||||||
|
'',
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildModernStatItem('服务评分', 'Driver rating', "4.9分", ''),
|
_buildModernStatItem(
|
||||||
|
'服务评分',
|
||||||
|
'Driver rating',
|
||||||
|
StorageService.to.hasVehicleInfo ? "4.9分" : '-',
|
||||||
|
'',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -300,20 +316,20 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: const LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: 0.75,
|
value: StorageService.to.hasVehicleInfo ? 0.75 : 0,
|
||||||
minHeight: 8,
|
minHeight: 8,
|
||||||
backgroundColor: Color(0xFFF0F2F5),
|
backgroundColor: Color(0xFFF0F2F5),
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(16, 185, 129, 1)),
|
valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(16, 185, 129, 1)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text("H2 Level", style: TextStyle(fontSize: 11, color: Colors.grey)),
|
Text("H2 Level", style: TextStyle(fontSize: 11, color: Colors.grey)),
|
||||||
Text(
|
Text(
|
||||||
"75%",
|
StorageService.to.hasVehicleInfo ? "75%" : "0%",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color.fromRGBO(16, 185, 129, 1),
|
color: Color.fromRGBO(16, 185, 129, 1),
|
||||||
@@ -353,7 +369,7 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
children: [
|
children: [
|
||||||
_buildCertificateContent('行驶证', controller.drivingAttachments),
|
_buildCertificateContent('行驶证', controller.drivingAttachments),
|
||||||
_buildCertificateContent('营运证', controller.operationAttachments),
|
_buildCertificateContent('营运证', controller.operationAttachments),
|
||||||
_buildCertificateContent('加氢资格证', controller.hydrogenationAttachments),
|
_buildCertificateContent('加氢证', controller.hydrogenationAttachments),
|
||||||
_buildCertificateContent('登记证', controller.registerAttachments),
|
_buildCertificateContent('登记证', controller.registerAttachments),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -373,7 +389,7 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0),
|
||||||
child: attachments.isEmpty
|
child: attachments.isEmpty
|
||||||
? const Center(child: Text('暂无相关证件信息'))
|
? _buildEmptyCertificateState(title)
|
||||||
: Column(
|
: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -382,7 +398,11 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: false),
|
_buildCertDetailItem(
|
||||||
|
'所属公司',
|
||||||
|
controller.rentFromCompany,
|
||||||
|
isFull: false,
|
||||||
|
),
|
||||||
_buildCertDetailItem('运营城市', controller.address),
|
_buildCertDetailItem('运营城市', controller.address),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -421,6 +441,158 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyCertificateState(String title) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/ic_attention@2x.png', // 请替换为您的实际图片路径
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'您未上传“$title”',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color.fromRGBO(51, 51, 51, 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'上传后可提前通知加氢站报备\n大幅减少加氢站等待时间',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color: Color.fromRGBO(156, 163, 175, 1),
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 44,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
_showUploadDialog(title);
|
||||||
|
},
|
||||||
|
icon: const Text(
|
||||||
|
'立即上传',
|
||||||
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
label: Image.asset(
|
||||||
|
'assets/images/ic_upload@2x.png',
|
||||||
|
height: 20.h,
|
||||||
|
width: 20.w,
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF017137),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: StadiumBorder(),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showUploadDialog(String title) {
|
||||||
|
Get.dialog(
|
||||||
|
Dialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'上传$title',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'请确保拍摄证件清晰可见,关键文字无反光遮挡,这将有助于快速通过审核',
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.grey[400], height: 1.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildUploadOption(
|
||||||
|
icon: Icons.camera_alt_outlined,
|
||||||
|
label: '拍照上传',
|
||||||
|
onTap: () {
|
||||||
|
controller.pickImage(title, ImageSource.camera);
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: _buildUploadOption(
|
||||||
|
icon: Icons.image_outlined,
|
||||||
|
label: '相册上传',
|
||||||
|
onTap: () {
|
||||||
|
controller.pickImage(title, ImageSource.gallery);
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建弹窗内的选择按钮
|
||||||
|
Widget _buildUploadOption({
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF2F9F7), // 浅绿色背景
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFF017137), // 深绿色圆圈
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: Colors.white, size: 28),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF333333),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildCertDetailItem(
|
Widget _buildCertDetailItem(
|
||||||
String label,
|
String label,
|
||||||
String value, {
|
String value, {
|
||||||
|
|||||||
113
ln_jq_app/lib/pages/c_page/mall/detail/controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
281
ln_jq_app/lib/pages/c_page/mall/detail/view.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
ln_jq_app/lib/pages/c_page/mall/exchange_success/view.dart
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
160
ln_jq_app/lib/pages/c_page/mall/mall_controller.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
345
ln_jq_app/lib/pages/c_page/mall/mall_view.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
ln_jq_app/lib/pages/c_page/mall/orders/controller.dart
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
|
||||||
|
import 'dart:developer';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
|
|
||||||
|
class OrderModel {
|
||||||
|
final int id;
|
||||||
|
final int scoreGoodsId;
|
||||||
|
final String goodsName;
|
||||||
|
final String? goodsImage;
|
||||||
|
final String? goodsContent;
|
||||||
|
final String address;
|
||||||
|
final String createTime;
|
||||||
|
final String score;
|
||||||
|
|
||||||
|
OrderModel({
|
||||||
|
required this.id,
|
||||||
|
required this.scoreGoodsId,
|
||||||
|
required this.goodsName,
|
||||||
|
this.goodsImage,
|
||||||
|
this.goodsContent,
|
||||||
|
required this.address,
|
||||||
|
required this.createTime,
|
||||||
|
required this.score,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory OrderModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return OrderModel(
|
||||||
|
id: json['id'] as int,
|
||||||
|
scoreGoodsId: json['scoreGoodsId'] as int,
|
||||||
|
goodsName: json['goodsName']?.toString() ?? '',
|
||||||
|
goodsImage: json['goodsImage'],
|
||||||
|
goodsContent: json['goodsContent'],
|
||||||
|
address: json['address']?.toString() ?? '',
|
||||||
|
createTime: json['createTime']?.toString() ?? '',
|
||||||
|
score: json['score']?.toString() ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MallOrdersController extends GetxController with BaseControllerMixin {
|
||||||
|
@override
|
||||||
|
String get builderId => 'mall_orders';
|
||||||
|
|
||||||
|
final RxList<OrderModel> orderList = <OrderModel>[].obs;
|
||||||
|
final RxBool isLoading = true.obs;
|
||||||
|
int pageNum = 1;
|
||||||
|
final int pageSize = 50;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
getOrders();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> getOrders({bool isRefresh = true}) async {
|
||||||
|
if (isRefresh) {
|
||||||
|
pageNum = 1;
|
||||||
|
isLoading.value = true;
|
||||||
|
updateUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await HttpService.to.post(
|
||||||
|
'appointment/score/getScoreExchangeList',
|
||||||
|
data: {
|
||||||
|
"status": "",
|
||||||
|
"pageNum": pageNum.toString(),
|
||||||
|
"pageSize": pageSize.toString()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response != null && response.data != null) {
|
||||||
|
var result = BaseModel<Map<String, dynamic>>.fromJson(response.data);
|
||||||
|
if (result.code == 0 && result.data != null) {
|
||||||
|
var records = result.data!['records'] as List;
|
||||||
|
var list = records.map((e) => OrderModel.fromJson(e)).toList();
|
||||||
|
if (isRefresh) {
|
||||||
|
orderList.assignAll(list);
|
||||||
|
} else {
|
||||||
|
orderList.addAll(list);
|
||||||
|
}
|
||||||
|
pageNum++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('获取订单列表失败: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
updateUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
ln_jq_app/lib/pages/c_page/mall/orders/view.dart
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'controller.dart';
|
||||||
|
|
||||||
|
class MallOrdersPage extends GetView<MallOrdersController> {
|
||||||
|
const MallOrdersPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GetBuilder<MallOrdersController>(
|
||||||
|
init: MallOrdersController(),
|
||||||
|
id: 'mall_orders',
|
||||||
|
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: RefreshIndicator(
|
||||||
|
onRefresh: () => controller.getOrders(isRefresh: true),
|
||||||
|
child: controller.isLoading.value && controller.orderList.isEmpty
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _buildOrderList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOrderList() {
|
||||||
|
if (controller.orderList.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text('暂无订单记录', style: TextStyle(color: Color(0xFF999999))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: controller.orderList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final order = controller.orderList[index];
|
||||||
|
return _buildOrderItem(order);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOrderItem(OrderModel order) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'兑换时间:${order.createTime}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color.fromRGBO(107, 114, 128, 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: order.goodsImage != null
|
||||||
|
? Image.network(
|
||||||
|
order.goodsImage!,
|
||||||
|
width: 80.w,
|
||||||
|
height: 80.h,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
width: 80.w,
|
||||||
|
height: 80.h,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Icon(Icons.image, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
order.goodsName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF333333),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
order.score,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.sp,
|
||||||
|
color: Color(0xFF4CAF50),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'积分',
|
||||||
|
style: TextStyle(fontSize: 11.sp, color: Color(0xFF4CAF50)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'x1',
|
||||||
|
style: TextStyle(color: Color(0xFFCCCCCC), fontSize: 16.sp),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
131
ln_jq_app/lib/pages/c_page/mall/rule/view.dart
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
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 MallRulePage extends StatelessWidget {
|
||||||
|
const MallRulePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Color.fromRGBO(64, 199, 154, 1),
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios, color: Colors.white, size: 20),
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// 顶部装饰图
|
||||||
|
Positioned(
|
||||||
|
top: 30,
|
||||||
|
right: Get.width * 0.15,
|
||||||
|
child: LoginUtil.getAssImg("rule_bg@2x"),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.fromLTRB(20, 100, 20, 20),
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/images/rule_bg_1@2x.png'),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'积分获取规则',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF2C3E50),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
_buildRuleItem(
|
||||||
|
icon: 'tips_1@2x',
|
||||||
|
title: '每日首次签到积分规则',
|
||||||
|
content: '每日首签,立得 1 积分',
|
||||||
|
),
|
||||||
|
_buildRuleItem(
|
||||||
|
icon: 'tips_2@2x',
|
||||||
|
title: '每日预约加氢积分规则',
|
||||||
|
content: '每日前 2 次预约加氢,各得 1 积分',
|
||||||
|
),
|
||||||
|
_buildRuleItem(
|
||||||
|
icon: 'tips_3@2x',
|
||||||
|
title: '连续签到累计赠分规则',
|
||||||
|
content: '连续签到 3 天赠 2 积分,7 天赠 5 积分',
|
||||||
|
),
|
||||||
|
_buildRuleItem(
|
||||||
|
icon: 'tips_4@2x',
|
||||||
|
title: '连续签到周期及断签重置规则',
|
||||||
|
content: '7 天为一个签到周期,中途断签则重新从第 1 天计算',
|
||||||
|
),
|
||||||
|
_buildRuleItem(
|
||||||
|
icon: 'tips_5@2x',
|
||||||
|
title: '积分使用规则',
|
||||||
|
content:
|
||||||
|
'积分有效期3个月。个人账户内累计的所有有效积分,可在平台积分商城中,用于兑换商城内上架的各类商品、权益或服务,兑换时将按照商品标注的积分值扣除对应积分,积分兑换后不支持撤销、退换,商品兑换规则以积分商城内公示为准。',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'本活动最终解释权归官方所有,如有疑问可咨询客服。',
|
||||||
|
style: TextStyle(color: Color(0xFF999999), fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRuleItem({
|
||||||
|
required String icon,
|
||||||
|
required String title,
|
||||||
|
required String content,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
LoginUtil.getAssImg(icon),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF333333),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 0),
|
||||||
|
child: Text(
|
||||||
|
content,
|
||||||
|
style: TextStyle(fontSize: 13.sp, color: Color(0xFF666666), height: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,38 +12,61 @@ class MessagePage extends GetView<MessageController> {
|
|||||||
Get.put(MessageController());
|
Get.put(MessageController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: const Color(0xFFF7F9FB),
|
||||||
appBar: AppBar(title: const Text('消息通知'), centerTitle: true),
|
appBar: AppBar(
|
||||||
body: Column(
|
title: const Text('消息通知'),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios, size: 20),
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Obx(() => SmartRefresher(
|
||||||
child: Obx(() => SmartRefresher(
|
|
||||||
controller: controller.refreshController,
|
controller: controller.refreshController,
|
||||||
enablePullUp: true,
|
enablePullUp: true,
|
||||||
onRefresh: controller.onRefresh,
|
onRefresh: controller.onRefresh,
|
||||||
onLoading: controller.onLoading,
|
onLoading: controller.onLoading,
|
||||||
child: ListView.separated(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||||
itemCount: controller.messageList.length,
|
itemCount: controller.messageList.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return _buildMessageItem(context, controller.messageList[index]);
|
return _buildMessageItem(context, controller.messageList[index]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
),
|
|
||||||
Obx(() => !controller.allRead.value
|
Obx(() => !controller.allRead.value
|
||||||
? Container(
|
? Positioned(
|
||||||
padding: const EdgeInsets.all(16),
|
right: 20,
|
||||||
color: Colors.white,
|
bottom: 50,
|
||||||
child: ElevatedButton(
|
child: GestureDetector(
|
||||||
onPressed: controller.markAllRead,
|
onTap: controller.markAllRead,
|
||||||
style: ElevatedButton.styleFrom(
|
child: Container(
|
||||||
backgroundColor: Colors.blue,
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||||
minimumSize: const Size(double.infinity, 44),
|
decoration: BoxDecoration(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
color: const Color(0xFF007A45),
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'全部已读',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Text('全部标为已读', style: TextStyle(fontSize: 16, color: Colors.white)),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink()),
|
: const SizedBox.shrink()),
|
||||||
@@ -53,55 +76,93 @@ class MessagePage extends GetView<MessageController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMessageItem(BuildContext context, MessageModel item) {
|
Widget _buildMessageItem(BuildContext context, MessageModel item) {
|
||||||
return GestureDetector(
|
return IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// 左侧时间轴线条和圆点
|
||||||
|
SizedBox(
|
||||||
|
width: 40,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 1.5,
|
||||||
|
color: const Color(0xFFD8E2EE),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 25,
|
||||||
|
child: Container(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: item.isRead == 1
|
||||||
|
? const Color(0xFFAAB6C3)
|
||||||
|
: const Color(0xFF4CAF50),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 右侧内容卡片
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
controller.markRead(item);
|
controller.markRead(item);
|
||||||
_showMessageDialog(context, item);
|
_showMessageDialog(context, item);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.02),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
child: Row(
|
],
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(top: 6, right: 12),
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: item.isRead == 1 ? Colors.grey[300] : const Color(0xFFFAAD14),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.title,
|
item.title,
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87),
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF333333),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
item.content,
|
item.content,
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
style: const TextStyle(
|
||||||
maxLines: 2,
|
fontSize: 14,
|
||||||
overflow: TextOverflow.ellipsis,
|
color: Color(0xFF666666),
|
||||||
|
height: 1.4,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
item.createTime,
|
item.createTime,
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
style: const TextStyle(
|
||||||
),
|
fontSize: 12,
|
||||||
],
|
color: Color(0xFFCCCCCC),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +172,7 @@ class MessagePage extends GetView<MessageController> {
|
|||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Dialog(
|
return Dialog(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -122,22 +183,22 @@ class MessagePage extends GetView<MessageController> {
|
|||||||
item.title,
|
item.title,
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
item.content,
|
item.content,
|
||||||
style: const TextStyle(fontSize: 15, height: 1.5, color: Colors.black87),
|
style: const TextStyle(
|
||||||
|
fontSize: 15, height: 1.5, color: Color(0xFF333333)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: OutlinedButton(
|
child: TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: OutlinedButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
side: const BorderSide(color: Colors.blue),
|
foregroundColor: const Color(0xFF007A45),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
|
||||||
),
|
),
|
||||||
child: const Text('确认', style: TextStyle(color: Colors.blue)),
|
child: const Text('确认',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/common/index.dart';
|
import 'package:getx_scaffold/common/index.dart';
|
||||||
import 'package:getx_scaffold/common/widgets/index.dart';
|
import 'package:getx_scaffold/common/widgets/index.dart';
|
||||||
import 'package:ln_jq_app/common/login_util.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/c_page/message/view.dart';
|
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
||||||
|
import 'package:ln_jq_app/pages/common/webview/view.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
|
|
||||||
@@ -38,6 +43,30 @@ class MinePage extends GetView<MineController> {
|
|||||||
_buildSafetyReminderCard(),
|
_buildSafetyReminderCard(),
|
||||||
SizedBox(height: 24.h),
|
SizedBox(height: 24.h),
|
||||||
_buildLogoutButton(),
|
_buildLogoutButton(),
|
||||||
|
SizedBox(height: 15.h),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: '《用户协议》',
|
||||||
|
style: TextStyle(color: Colors.blue, fontSize: 13),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
openPage("用户协议", "https://lnh2e.com/user_agreement.html");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: '《隐私政策》',
|
||||||
|
style: TextStyle(color: Colors.blue, fontSize: 13),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
SizedBox(height: 95.h),
|
SizedBox(height: 95.h),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -51,6 +80,15 @@ class MinePage extends GetView<MineController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void openPage(String title, String url) {
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
openWebPage(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Get.to(() => const WebViewPage(), arguments: {'title': title, 'url': url});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 构建顶部用户信息卡片
|
/// 构建顶部用户信息卡片
|
||||||
Widget _buildUserInfoCard() {
|
Widget _buildUserInfoCard() {
|
||||||
return Card(
|
return Card(
|
||||||
|
|||||||
@@ -69,239 +69,34 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
String get formattedTimeSlot =>
|
String get formattedTimeSlot =>
|
||||||
'${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}';
|
'${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}';
|
||||||
|
|
||||||
void pickDate(BuildContext context) {
|
|
||||||
DateTime tempDate = selectedDate.value;
|
|
||||||
|
|
||||||
// 获取今天的日期 (不含时间)
|
|
||||||
final DateTime today = DateTime(
|
|
||||||
DateTime.now().year,
|
|
||||||
DateTime.now().month,
|
|
||||||
DateTime.now().day,
|
|
||||||
);
|
|
||||||
|
|
||||||
//计算明天的日期
|
|
||||||
final DateTime tomorrow = today.add(const Duration(days: 1));
|
|
||||||
|
|
||||||
Get.bottomSheet(
|
|
||||||
Container(
|
|
||||||
height: 300,
|
|
||||||
padding: const EdgeInsets.only(top: 6.0),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
CupertinoButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
child: const Text(
|
|
||||||
'取消',
|
|
||||||
style: TextStyle(color: CupertinoColors.systemGrey),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CupertinoButton(
|
|
||||||
onPressed: () {
|
|
||||||
final bool isChangingToToday =
|
|
||||||
tempDate.isAtSameMomentAs(today) &&
|
|
||||||
!selectedDate.value.isAtSameMomentAs(today);
|
|
||||||
final bool isDateChanged = !tempDate.isAtSameMomentAs(
|
|
||||||
selectedDate.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 更新选中的日期
|
|
||||||
selectedDate.value = tempDate;
|
|
||||||
Get.back(); // 先关闭弹窗
|
|
||||||
|
|
||||||
// 如果日期发生了变化,则重置时间
|
|
||||||
if (isDateChanged) {
|
|
||||||
resetTimeForSelectedDate();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'确认',
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppTheme.themeColor,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1, color: Color(0xFFE5E5E5)),
|
|
||||||
Expanded(
|
|
||||||
child: CupertinoDatePicker(
|
|
||||||
mode: CupertinoDatePickerMode.date,
|
|
||||||
initialDateTime: selectedDate.value,
|
|
||||||
minimumDate: today,
|
|
||||||
// 最小可选日期为今天
|
|
||||||
maximumDate: tomorrow,
|
|
||||||
// 最大可选日期为明天
|
|
||||||
// ---------------------
|
|
||||||
onDateTimeChanged: (DateTime newDate) {
|
|
||||||
tempDate = newDate;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void resetTimeForSelectedDate() {
|
void resetTimeForSelectedDate() {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
// 判断新选择的日期是不是今天
|
// 1. 获取当前站点
|
||||||
|
final station = stationOptions.firstWhereOrNull(
|
||||||
|
(s) => s.hydrogenId == selectedStationId.value,
|
||||||
|
);
|
||||||
|
if (station == null) return;
|
||||||
|
|
||||||
|
// 2. 解析营业开始和结束的小时
|
||||||
|
final bizStartHour = int.tryParse(station.startBusiness.split(':')[0]) ?? 0;
|
||||||
|
final bizEndHour = int.tryParse(station.endBusiness.split(':')[0]) ?? 23;
|
||||||
|
|
||||||
if (selectedDate.value.isAtSameMomentAs(today)) {
|
if (selectedDate.value.isAtSameMomentAs(today)) {
|
||||||
// 如果是今天,就将时间重置为当前时间所在的半小时区间
|
// 如果是今天:起始时间 = max(当前小时, 营业开始小时),且上限为营业结束小时
|
||||||
startTime.value = _calculateInitialStartTime(now);
|
int targetHour = now.hour;
|
||||||
endTime.value = TimeOfDay.fromDateTime(
|
if (targetHour < bizStartHour) targetHour = bizStartHour;
|
||||||
_getDateTimeFromTimeOfDay(startTime.value).add(const Duration(minutes: 30)),
|
if (targetHour > bizEndHour) targetHour = bizEndHour;
|
||||||
);
|
|
||||||
|
startTime.value = TimeOfDay(hour: targetHour, minute: 0);
|
||||||
} else {
|
} else {
|
||||||
// 如果是明天(或其他未来日期),则可以将时间重置为一天的最早可用时间,例如 00:00
|
// 如果是明天:起始时间直接重置为营业开始小时
|
||||||
startTime.value = const TimeOfDay(hour: 0, minute: 0);
|
startTime.value = TimeOfDay(hour: bizStartHour, minute: 0);
|
||||||
endTime.value = const TimeOfDay(hour: 0, minute: 30);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///60 分钟为间隔 时间选择器
|
// 结束时间默认顺延1小时
|
||||||
void pickTime(BuildContext context) {
|
endTime.value = TimeOfDay(hour: (startTime.value.hour + 1) % 24, minute: 0);
|
||||||
final now = DateTime.now();
|
|
||||||
final isToday =
|
|
||||||
selectedDate.value.year == now.year &&
|
|
||||||
selectedDate.value.month == now.month &&
|
|
||||||
selectedDate.value.day == now.day;
|
|
||||||
|
|
||||||
final List<TimeSlot> availableSlots = [];
|
|
||||||
for (int i = 0; i < 24; i++) {
|
|
||||||
// 每次增加 60 分钟
|
|
||||||
final startMinutes = i * 60;
|
|
||||||
final endMinutes = startMinutes + 60;
|
|
||||||
|
|
||||||
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
|
|
||||||
// 注意:endMinutes % 60 始终为 0,因为间隔是整小时
|
|
||||||
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
|
|
||||||
|
|
||||||
// 如果不是今天,所有时间段都有效
|
|
||||||
if (!isToday) {
|
|
||||||
availableSlots.add(TimeSlot(startTime, endTime));
|
|
||||||
} else {
|
|
||||||
// 如果是今天,需要判断该时间段是否可选
|
|
||||||
// 创建时间段的结束时间对象
|
|
||||||
final slotEndDateTime = DateTime(
|
|
||||||
selectedDate.value.year,
|
|
||||||
selectedDate.value.month,
|
|
||||||
selectedDate.value.day,
|
|
||||||
endTime.hour,
|
|
||||||
endTime.minute,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 注意:如果是跨天的 00:00 (例如 23:00 - 00:00),需要将日期加一天,否则 isAfter 判断会出错
|
|
||||||
// 但由于我们用的是 endTime.hour % 24,当变成 0 时,日期还是 selectedDate
|
|
||||||
// 这里做一个特殊处理:如果 endTime 是 00:00,意味着它实际上是明天的开始
|
|
||||||
DateTime realEndDateTime = slotEndDateTime;
|
|
||||||
if (endTime.hour == 0 && endTime.minute == 0) {
|
|
||||||
realEndDateTime = slotEndDateTime.add(const Duration(days: 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只要时间段的结束时间晚于当前时间,这个时间段就是可预约的
|
|
||||||
if (realEndDateTime.isAfter(now)) {
|
|
||||||
availableSlots.add(TimeSlot(startTime, endTime));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (availableSlots.isEmpty) {
|
|
||||||
showToast('今天已没有可预约的时间段');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找当前选中的时间对应的新列表中的索引
|
|
||||||
int initialItem = availableSlots.indexWhere(
|
|
||||||
(slot) => slot.start.hour == startTime.value.hour,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (initialItem == -1) {
|
|
||||||
initialItem = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeSlot tempSlot = availableSlots[initialItem];
|
|
||||||
|
|
||||||
final FixedExtentScrollController scrollController = FixedExtentScrollController(
|
|
||||||
initialItem: initialItem,
|
|
||||||
);
|
|
||||||
|
|
||||||
Get.bottomSheet(
|
|
||||||
Container(
|
|
||||||
height: 300,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
CupertinoButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
child: const Text(
|
|
||||||
'取消',
|
|
||||||
style: TextStyle(color: CupertinoColors.systemGrey),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CupertinoButton(
|
|
||||||
onPressed: () {
|
|
||||||
startTime.value = tempSlot.start;
|
|
||||||
endTime.value = tempSlot.end;
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'确认',
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppTheme.themeColor,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1, color: Color(0xFFE5E5E5)),
|
|
||||||
Expanded(
|
|
||||||
child: CupertinoPicker(
|
|
||||||
scrollController: scrollController,
|
|
||||||
itemExtent: 40.0,
|
|
||||||
onSelectedItemChanged: (index) {
|
|
||||||
tempSlot = availableSlots[index];
|
|
||||||
},
|
|
||||||
children: availableSlots
|
|
||||||
.map((slot) => Center(child: Text(slot.display)))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用于存储上一次成功预约的信息
|
// 用于存储上一次成功预约的信息
|
||||||
@@ -426,8 +221,15 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
stateName: '',
|
stateName: '',
|
||||||
addStatus: '',
|
addStatus: '',
|
||||||
addStatusName: '',
|
addStatusName: '',
|
||||||
rejectReason: '',
|
|
||||||
hasEdit: true,
|
hasEdit: true,
|
||||||
|
rejectReason: '',
|
||||||
|
isTruckAttachment: 0,
|
||||||
|
hasHydrogenationAttachment: true,
|
||||||
|
hasDrivingAttachment: true,
|
||||||
|
isEdit: '',
|
||||||
|
drivingAttachments: [],
|
||||||
|
hydrogenationAttachments: [],
|
||||||
|
gunNumber: '',
|
||||||
);
|
);
|
||||||
|
|
||||||
//打开预约列表
|
//打开预约列表
|
||||||
@@ -458,7 +260,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
if (_debounce?.isActive ?? false) {
|
if (_debounce?.isActive ?? false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_debounce = Timer(const Duration(seconds: 1), () {});
|
_debounce = Timer(const Duration(milliseconds: 200), () {});
|
||||||
|
|
||||||
showLoading("加载中");
|
showLoading("加载中");
|
||||||
|
|
||||||
@@ -536,12 +338,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String workEfficiency = "0";
|
String workEfficiency = "-";
|
||||||
String fillingWeight = "0";
|
String fillingWeight = "-";
|
||||||
String fillingTimes = "0";
|
String fillingTimes = "-";
|
||||||
|
String modeImage = "";
|
||||||
String plateNumber = "";
|
String plateNumber = "";
|
||||||
String vin = "";
|
String vin = "";
|
||||||
String leftHydrogen = "0";
|
String leftHydrogen = "-";
|
||||||
num maxHydrogen = 0;
|
num maxHydrogen = 0;
|
||||||
String difference = "";
|
String difference = "";
|
||||||
var progressValue = 0.0;
|
var progressValue = 0.0;
|
||||||
@@ -597,7 +400,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
// 创建一个每1分钟执行一次的周期性定时器
|
// 创建一个每1分钟执行一次的周期性定时器
|
||||||
_refreshTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
|
_refreshTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
|
||||||
getSiteList();
|
getSiteList(showloading: false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,7 +453,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
try {
|
try {
|
||||||
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
||||||
var responseData = await HttpService.to.get(
|
var responseData = await HttpService.to.get(
|
||||||
'appointment/truck/history-filling-summary?vin=$vin',
|
'appointment/truck/history-filling-summary?vin=$vin&plateNumber=$plateNumber',
|
||||||
);
|
);
|
||||||
if (responseData == null || responseData.data == null) {
|
if (responseData == null || responseData.data == null) {
|
||||||
showToast('服务暂不可用,请稍后');
|
showToast('服务暂不可用,请稍后');
|
||||||
@@ -664,6 +467,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
fillingWeight = "$formatted${result.data["fillingWeightUnit"]}";
|
fillingWeight = "$formatted${result.data["fillingWeightUnit"]}";
|
||||||
fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}";
|
fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}";
|
||||||
|
modeImage = result.data["modeImage"].toString();
|
||||||
|
|
||||||
updateUi();
|
updateUi();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -689,8 +493,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
|
|
||||||
leftHydrogen = result.data["leftHydrogen"].toString();
|
leftHydrogen = "${result.data["leftHydrogen"]}Kg";
|
||||||
workEfficiency = result.data["workEfficiency"].toString();
|
workEfficiency = "${result.data["workEfficiency"]}Kg";
|
||||||
|
|
||||||
final leftHydrogenNum = double.tryParse(leftHydrogen) ?? 0.0;
|
final leftHydrogenNum = double.tryParse(leftHydrogen) ?? 0.0;
|
||||||
difference = (maxHydrogen - leftHydrogenNum).toStringAsFixed(2);
|
difference = (maxHydrogen - leftHydrogenNum).toStringAsFixed(2);
|
||||||
@@ -724,8 +528,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
updateUi();
|
updateUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
void getSiteList() async {
|
void getSiteList({showloading = true}) async {
|
||||||
if (StorageService.to.phone == "13344444444") {
|
if (StorageService.to.phone == "13344444444" ||
|
||||||
|
StorageService.to.phone == "13888888888") {
|
||||||
//该账号给stationOptions手动添加一个数据
|
//该账号给stationOptions手动添加一个数据
|
||||||
final testStation = StationModel(
|
final testStation = StationModel(
|
||||||
hydrogenId: '1142167389150920704',
|
hydrogenId: '1142167389150920704',
|
||||||
@@ -735,7 +540,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
// 价格
|
// 价格
|
||||||
siteStatusName: '营运中',
|
siteStatusName: '营运中',
|
||||||
// 状态
|
// 状态
|
||||||
isSelect: 1, // 默认可选
|
isSelect: 1,
|
||||||
|
startBusiness: '08:00:00',
|
||||||
|
endBusiness: '20:00:00', // 默认可选
|
||||||
);
|
);
|
||||||
// 使用 assignAll 可以确保列表只包含这个测试数据
|
// 使用 assignAll 可以确保列表只包含这个测试数据
|
||||||
stationOptions.assignAll([testStation]);
|
stationOptions.assignAll([testStation]);
|
||||||
@@ -747,7 +554,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (showloading) {
|
||||||
showLoading("加氢站数据加载中");
|
showLoading("加氢站数据加载中");
|
||||||
|
}
|
||||||
|
|
||||||
var responseData = await HttpService.to.get(
|
var responseData = await HttpService.to.get(
|
||||||
"appointment/station/queryHydrogenSiteInfo",
|
"appointment/station/queryHydrogenSiteInfo",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/common/index.dart';
|
import 'package:getx_scaffold/common/index.dart';
|
||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/reservation/controller.dart';
|
import 'package:ln_jq_app/pages/c_page/reservation/controller.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/reservation_edit/controller.dart';
|
import 'package:ln_jq_app/pages/c_page/reservation_edit/controller.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/reservation_edit/view.dart';
|
import 'package:ln_jq_app/pages/c_page/reservation_edit/view.dart';
|
||||||
@@ -36,19 +37,19 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
height: Get.height * 0.55,
|
height: Get.height * 0.6,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white,
|
color: Color.fromRGBO(247, 249, 251, 1),
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(16),
|
topLeft: Radius.circular(16),
|
||||||
topRight: Radius.circular(16),
|
topRight: Radius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 构建标题和下拉框
|
// 构建标题和下拉框
|
||||||
_buildHeader(),
|
_buildHeader(),
|
||||||
const Divider(height: 1),
|
|
||||||
// 下拉筛选框
|
// 下拉筛选框
|
||||||
_buildChoice(),
|
_buildChoice(),
|
||||||
// 构建列表(使用 Obx 监听数据变化)
|
// 构建列表(使用 Obx 监听数据变化)
|
||||||
@@ -58,35 +59,53 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Container _buildChoice() {
|
Widget _buildChoice() {
|
||||||
return Container(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 8, 0, 0),
|
scrollDirection: Axis.horizontal,
|
||||||
alignment: AlignmentGeometry.centerLeft,
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: _statusOptions.entries.map((entry) {
|
||||||
|
bool isSelected = _selectedStatus == entry.key;
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
if (_selectedStatus == entry.key) return;
|
||||||
|
|
||||||
|
// 立即执行刷新逻辑
|
||||||
|
_controller.getReservationList(addStatus: entry.key);
|
||||||
|
|
||||||
|
// 先更新本地状态改变 UI 选中效果
|
||||||
|
setState(() {
|
||||||
|
_selectedStatus = entry.key;
|
||||||
|
});
|
||||||
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
margin: const EdgeInsets.only(right: 12),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[100],
|
// 选中色使用深绿色,未选中保持纯白
|
||||||
borderRadius: BorderRadius.circular(8),
|
color: isSelected ? const Color(0xFF006633) : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.04),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
entry.key == '' ? '全部' : entry.value,
|
||||||
|
style: TextStyle(
|
||||||
|
// 未选中文字颜色微调为图片中的灰蓝色
|
||||||
|
color: isSelected ? Colors.white : const Color(0xFFAAB6C3),
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: _selectedStatus,
|
|
||||||
underline: const SizedBox.shrink(), // 隐藏下划线
|
|
||||||
items: _statusOptions.entries.map((entry) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: entry.key,
|
|
||||||
child: Text(entry.value),
|
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (newValue) {
|
|
||||||
if (newValue != null) {
|
|
||||||
setState(() {
|
|
||||||
_selectedStatus = newValue;
|
|
||||||
});
|
|
||||||
// 当选择新状态时,调用接口刷新数据
|
|
||||||
_controller.getReservationList(addStatus: _selectedStatus);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -94,24 +113,10 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
|
|||||||
/// 构建标题、关闭按钮和下拉筛选框
|
/// 构建标题、关闭按钮和下拉筛选框
|
||||||
Widget _buildHeader() {
|
Widget _buildHeader() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 8, 8, 8),
|
margin: const EdgeInsets.fromLTRB(20, 20, 8, 8),
|
||||||
child: Stack(
|
child: const Text(
|
||||||
alignment: Alignment.center,
|
'我的预约',
|
||||||
children: [
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
Center(child: const Text('我的预约', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
elevation: 0,
|
|
||||||
backgroundColor: Colors.grey[200],
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
|
||||||
),
|
|
||||||
child: const Text('关闭', style: TextStyle(color: Colors.black54)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -134,8 +139,8 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
|
|||||||
return Card(
|
return Card(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
margin: const EdgeInsets.only(bottom: 12.0),
|
margin: const EdgeInsets.only(bottom: 12.0),
|
||||||
elevation: 1,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -151,17 +156,20 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
|
|||||||
vertical: 5,
|
vertical: 5,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.shade50, // 淡蓝色背景
|
color: reservation.state == "-1"
|
||||||
|
? Color.fromRGBO(241, 67, 56, 0.1)
|
||||||
|
: Color.fromRGBO(230, 249, 243, 1),
|
||||||
borderRadius: BorderRadius.circular(4), // 小圆角
|
borderRadius: BorderRadius.circular(4), // 小圆角
|
||||||
// 可以选择去掉边框,或者用极淡的边框
|
// 可以选择去掉边框,或者用极淡的边框
|
||||||
border: Border.all(color: Colors.blue.shade100),
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
"${reservation.stateName}-${reservation.addStatusName}",
|
"${reservation.stateName}-${reservation.addStatusName}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.blue.shade700,
|
color: reservation.state == "-1"
|
||||||
fontSize: 12,
|
? Color.fromRGBO(241, 67, 56, 0.8)
|
||||||
fontWeight: FontWeight.w500,
|
: Color.fromRGBO(49, 186, 133, 1),
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -171,12 +179,10 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
height: 28, // 限制按钮高度,显得精致
|
height: 28, // 限制按钮高度,显得精致
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () async{
|
onPressed: () async {
|
||||||
var responseData = await HttpService.to.post(
|
var responseData = await HttpService.to.post(
|
||||||
'appointment/orderAddHyd/vehicle-cancel',
|
'appointment/orderAddHyd/vehicle-cancel',
|
||||||
data: {
|
data: {'id': reservation.id},
|
||||||
'id': reservation.id,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (responseData == null || responseData.data == null) {
|
if (responseData == null || responseData.data == null) {
|
||||||
@@ -198,22 +204,25 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
|
|||||||
},
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
side: BorderSide(color: Colors.grey.shade400), // 灰色边框
|
side: BorderSide(color: Colors.grey.shade400),
|
||||||
shape: const StadiumBorder(), // 胶囊形状
|
shape: const StadiumBorder(),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'取消预约',
|
'取消预约',
|
||||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontSize: 11.sp,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 10.w,),
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
// 修改按钮 (仅在 hasEdit 为 true 时显示)
|
// 修改按钮 (仅在 hasEdit 为 true 时显示)
|
||||||
if (reservation.hasEdit)
|
if (reservation.hasEdit)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 28,
|
height: 28,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () async{
|
onPressed: () async {
|
||||||
var result = await Get.to(
|
var result = await Get.to(
|
||||||
() => ReservationEditPage(),
|
() => ReservationEditPage(),
|
||||||
arguments: {
|
arguments: {
|
||||||
@@ -233,21 +242,22 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
|
|||||||
},
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
side: const BorderSide(color: Colors.blue), // 蓝色边框
|
side: BorderSide(color: AppTheme.themeColor),
|
||||||
shape: const StadiumBorder(),
|
shape: const StadiumBorder(),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'修改',
|
'修改',
|
||||||
style: TextStyle(color: Colors.blue, fontSize: 12),
|
style: TextStyle(color: AppTheme.themeColor, fontSize: 11.sp),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildDetailRow('车牌号:', reservation.plateNumber),
|
_buildDetailRow('车牌号:', reservation.plateNumber),
|
||||||
_buildDetailRow('预约日期:', reservation.date),
|
_buildDetailRow('预约日期:', reservation.date),
|
||||||
_buildDetailRow('预约氢量:', reservation.hydAmount),
|
_buildDetailRow('预约氢量:', "${reservation.hydAmount} KG"),
|
||||||
_buildDetailRow('加氢站:', reservation.stationName),
|
_buildDetailRow('加氢站:', reservation.stationName),
|
||||||
_buildDetailRow('开始时间:', reservation.startTime),
|
_buildDetailRow('开始时间:', reservation.startTime),
|
||||||
_buildDetailRow('结束时间:', reservation.endTime),
|
_buildDetailRow('结束时间:', reservation.endTime),
|
||||||
@@ -271,11 +281,26 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: const TextStyle(color: Colors.grey)),
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color.fromRGBO(51, 51, 51, 0.6),
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Text(
|
||||||
child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: label.contains("预约氢量:")
|
||||||
|
? Color.fromRGBO(27, 168, 85, 1)
|
||||||
|
: Color.fromRGBO(51, 51, 51, 1),
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
Positioned(
|
Positioned(
|
||||||
left: 20.w,
|
left: 20.w,
|
||||||
right: 20.w,
|
right: 20.w,
|
||||||
bottom: 110.h,
|
bottom: Get.height * (Get.height < 826 ? 0.08 : 0.11),
|
||||||
child: _buildReservationItem(context),
|
child: _buildReservationItem(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -148,9 +148,11 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
controller.stopAutoRefresh();
|
||||||
var scanResult = await Get.to(() => const MessagePage());
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
if (scanResult == null) {
|
if (scanResult == null) {
|
||||||
controller.msgNotice();
|
controller.msgNotice();
|
||||||
|
controller.startAutoRefresh();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: Badge(
|
icon: Badge(
|
||||||
@@ -177,7 +179,12 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildModernStatItem('总加氢次数', '', controller.fillingTimes, ''),
|
_buildModernStatItem('总加氢次数', '', controller.fillingTimes, ''),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildModernStatItem('今日里程', '', "7kg", ''),
|
_buildModernStatItem(
|
||||||
|
'今日里程',
|
||||||
|
'',
|
||||||
|
StorageService.to.hasVehicleInfo ? "7kg" : "-",
|
||||||
|
'',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -237,17 +244,33 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(flex: 4, child: LoginUtil.getAssImg('ic_car_bg@2x')),
|
Expanded(
|
||||||
|
flex: 4,
|
||||||
|
child: Image.network(
|
||||||
|
controller.modeImage,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Center(child: LoginUtil.getAssImg('ic_car_select@2x'));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 6,
|
flex: 6,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildCarDataItem('剩余电量', '36.8%'),
|
_buildCarDataItem(
|
||||||
|
'剩余电量',
|
||||||
|
StorageService.to.hasVehicleInfo ? '36.8%' : '-',
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildCarDataItem('剩余氢量', '${controller.leftHydrogen}Kg'),
|
_buildCarDataItem('剩余氢量', controller.leftHydrogen),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildCarDataItem('百公里氢耗', '${controller.workEfficiency}Kg'),
|
_buildCarDataItem('百公里氢耗', controller.workEfficiency),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -275,7 +298,7 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"${controller.leftHydrogen}Kg",
|
controller.leftHydrogen,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Color(0xFF006633),
|
color: Color(0xFF006633),
|
||||||
@@ -434,8 +457,34 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
/// 时间 Slider 选择器
|
/// 时间 Slider 选择器
|
||||||
Widget _buildTimeSlider(BuildContext context) {
|
Widget _buildTimeSlider(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
// 这里的逻辑对应 Controller 中的 24 小时可用 Slot
|
// 获取站点信息
|
||||||
int currentIdx = controller.startTime.value.hour;
|
final station = controller.stationOptions.firstWhereOrNull(
|
||||||
|
(s) => s.hydrogenId == controller.selectedStationId.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果没有站点数据,默认隐藏
|
||||||
|
if (station == null) {
|
||||||
|
return const SizedBox(height: 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析营业范围
|
||||||
|
final startParts = station.startBusiness.split(':');
|
||||||
|
final endParts = station.endBusiness.split(':');
|
||||||
|
int bizStartHour = int.tryParse(startParts[0]) ?? 0;
|
||||||
|
int bizEndHour = int.tryParse(endParts[0]) ?? 23;
|
||||||
|
int bizEndMinute = (endParts.length > 1) ? (int.tryParse(endParts[1]) ?? 0) : 0;
|
||||||
|
if (bizEndMinute == 0 && bizEndHour > bizStartHour) bizEndHour--;
|
||||||
|
|
||||||
|
//确定当前滑块值
|
||||||
|
int currentHour = controller.startTime.value.hour;
|
||||||
|
double sliderValue = currentHour.toDouble().clamp(
|
||||||
|
bizStartHour.toDouble(),
|
||||||
|
bizEndHour.toDouble(),
|
||||||
|
);
|
||||||
|
|
||||||
|
double minVal = bizStartHour.toDouble();
|
||||||
|
double maxVal = bizEndHour.toDouble();
|
||||||
|
if (minVal >= maxVal) maxVal = minVal + 1;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -484,23 +533,20 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
overlayColor: const Color(0xFF006633).withOpacity(0.1),
|
overlayColor: const Color(0xFF006633).withOpacity(0.1),
|
||||||
),
|
),
|
||||||
child: Slider(
|
child: Slider(
|
||||||
value: currentIdx.toDouble(),
|
value: sliderValue,
|
||||||
min: 0,
|
min: minVal,
|
||||||
max: 23,
|
max: maxVal,
|
||||||
divisions: 23,
|
// divisions: bizEndHour - bizStartHour > 0 ? bizEndHour - bizStartHour : 1,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
int hour = val.toInt();
|
int hour = val.toInt();
|
||||||
// 模拟 Controller 中的 pickTime 逻辑校验
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final isToday =
|
final isToday =
|
||||||
controller.selectedDate.value.year == now.year &&
|
controller.selectedDate.value.year == now.year &&
|
||||||
controller.selectedDate.value.month == now.month &&
|
controller.selectedDate.value.month == now.month &&
|
||||||
controller.selectedDate.value.day == now.day;
|
controller.selectedDate.value.day == now.day;
|
||||||
|
|
||||||
if (isToday && hour < now.hour) {
|
// 如果是今天,判断不可选过去的时间点
|
||||||
// 如果是今天且小时数小于当前,则忽略
|
if (isToday && hour < now.hour) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.startTime.value = TimeOfDay(hour: hour, minute: 0);
|
controller.startTime.value = TimeOfDay(hour: hour, minute: 0);
|
||||||
controller.endTime.value = TimeOfDay(hour: (hour + 1) % 24, minute: 0);
|
controller.endTime.value = TimeOfDay(hour: (hour + 1) % 24, minute: 0);
|
||||||
@@ -627,6 +673,7 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
controller.selectedStationId.value = value;
|
controller.selectedStationId.value = value;
|
||||||
|
controller.resetTimeForSelectedDate();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
|
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_app_update/flutter_app_update.dart';
|
||||||
|
import 'package:flutter_app_update/result_model.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart';
|
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
|
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
|
||||||
@@ -20,107 +23,199 @@ class HomeController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
final _aliyunPush = AliyunPushFlutter();
|
final _aliyunPush = AliyunPushFlutter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get listenLifecycleEvent => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
initAliyunPush();
|
|
||||||
addPushCallback();
|
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
|
log('page-init');
|
||||||
|
|
||||||
|
// 页面初始化后执行版本检查
|
||||||
|
checkVersionInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
String downloadUrl = "";
|
||||||
|
|
||||||
|
/// 检查 App 更新信息,增加版本号比对逻辑
|
||||||
|
void checkVersionInfo() async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.to.get('appointment/appConfig/get');
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0 && result.data != null) {
|
||||||
|
final data = result.data as Map<String, dynamic>;
|
||||||
|
|
||||||
|
bool hasUpdate = data['hasUpdate']?.toString().toLowerCase() == "true";
|
||||||
|
bool isForce = data['isForce']?.toString().toLowerCase() == "true";
|
||||||
|
String versionName = data['versionName'] ?? "新版本";
|
||||||
|
String updateContent = data['updateContent'] ?? "优化系统性能,提升用户体验";
|
||||||
|
downloadUrl = data['downloadUrl'].toString();
|
||||||
|
|
||||||
|
// 获取服务器配置的目标构建号
|
||||||
|
int serverVersionCode =
|
||||||
|
int.tryParse(data['versionCode']?.toString() ?? "0") ?? 0;
|
||||||
|
int serverIosBuildId = int.tryParse(data['iosBuildId']?.toString() ?? "0") ?? 0;
|
||||||
|
|
||||||
|
// 获取本地当前的构建号
|
||||||
|
String currentBuildStr = await getBuildNumber();
|
||||||
|
int currentBuild = int.tryParse(currentBuildStr) ?? 0;
|
||||||
|
|
||||||
|
bool needUpdate = false;
|
||||||
|
if (GetPlatform.isAndroid) {
|
||||||
|
needUpdate = currentBuild < serverVersionCode;
|
||||||
|
} else if (GetPlatform.isIOS) {
|
||||||
|
needUpdate = currentBuild < serverIosBuildId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果服务器标记有更新,且本地版本号确实较低,则弹出更新
|
||||||
|
if (hasUpdate && needUpdate) {
|
||||||
|
_showUpdateDialog("版本:$versionName\n\n更新内容:\n$updateContent", isForce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.d("版本检查失败: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 显示更新弹窗
|
||||||
|
void _showUpdateDialog(String content, bool isForce) {
|
||||||
|
DialogX.to.showConfirmDialog(
|
||||||
|
title: '升级提醒',
|
||||||
|
confirmText: '立即升级',
|
||||||
|
content: _buildDialogContent(content),
|
||||||
|
// 如果是强制更新,取消按钮显示为空,即隐藏
|
||||||
|
cancelText: isForce ? "" : '以后再说',
|
||||||
|
// 设置为 false,禁止点击背景和物理返回键关闭
|
||||||
|
barrierDismissible: false,
|
||||||
|
onConfirm: () {
|
||||||
|
jumpUpdateApp();
|
||||||
|
|
||||||
|
// ios如果是强制更新,点击后维持弹窗,防止用户进入 App
|
||||||
|
if (isForce && GetPlatform.isIOS) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
_showUpdateDialog(content, isForce);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDialogContent(String content) {
|
||||||
|
return PopScope(
|
||||||
|
canPop: false, // 关键:禁止 pop
|
||||||
|
child: TextX.bodyMedium(content).padding(bottom: 16.h),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void jumpUpdateApp() {
|
||||||
|
if (GetPlatform.isIOS) {
|
||||||
|
// 跳转到 iOS 应用商店网页
|
||||||
|
openWebPage("https://apps.apple.com/cn/app/羚牛氢能/6756245815");
|
||||||
|
} else if (GetPlatform.isAndroid) {
|
||||||
|
// Android 执行下载安装流程
|
||||||
|
showAndroidDownloadDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showAndroidDownloadDialog() {
|
||||||
|
AzhonAppUpdate.listener((ResultModel model) {
|
||||||
|
if (model.type == ResultType.start) {
|
||||||
|
DialogX.to.showConfirmDialog(
|
||||||
|
content: PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextX.bodyMedium('升级中...').padding(bottom: 45.h),
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
confirmText: '',
|
||||||
|
cancelText: "",
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
} else if (model.type == ResultType.done) {
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
UpdateModel model = UpdateModel(downloadUrl, "xll.apk", "logo", '正在下载最新版本...');
|
||||||
|
AzhonAppUpdate.update(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据登录状态和登录渠道返回不同的首页
|
// 根据登录状态和登录渠道返回不同的首页
|
||||||
Widget getHomePage() {
|
Widget getHomePage() {
|
||||||
requestPermission();
|
|
||||||
//登录状态跳转
|
|
||||||
if (StorageService.to.isLoggedIn) {
|
if (StorageService.to.isLoggedIn) {
|
||||||
// 如果已登录,再判断是哪个渠道
|
// 检查是否同意过隐私政策,只有同意后才初始化推送
|
||||||
|
if (StorageService.to.isPrivacyAgreed) {
|
||||||
|
requestPermission();
|
||||||
|
initAliyunPush();
|
||||||
|
addPushCallback();
|
||||||
|
}
|
||||||
|
|
||||||
if (StorageService.to.loginChannel == LoginChannel.station) {
|
if (StorageService.to.loginChannel == LoginChannel.station) {
|
||||||
return B_BaseWidgetsPage(); // 站点首页
|
return B_BaseWidgetsPage();
|
||||||
} else if (StorageService.to.loginChannel == LoginChannel.driver) {
|
} else if (StorageService.to.loginChannel == LoginChannel.driver) {
|
||||||
return BaseWidgetsPage(); // 司机首页
|
return BaseWidgetsPage();
|
||||||
} else {
|
} else {
|
||||||
return LoginPage();
|
return LoginPage();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 未登录,直接去登录页
|
|
||||||
return LoginPage();
|
return LoginPage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void requestPermission() async {
|
void requestPermission() async {
|
||||||
PermissionStatus status = await Permission.notification.status;
|
PermissionStatus status = await Permission.notification.status;
|
||||||
if (status.isGranted) {
|
if (status.isGranted) return;
|
||||||
Logger.d("通知权限已开启");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.isDenied) {
|
if (status.isDenied) {
|
||||||
// 建议此处增加一个应用内的 Rationale (解释说明) 弹窗
|
|
||||||
status = await Permission.notification.request();
|
status = await Permission.notification.request();
|
||||||
}
|
}
|
||||||
if (status.isGranted) {
|
if (status.isGranted) {
|
||||||
// 授权成功
|
|
||||||
Logger.d('通知已开启');
|
Logger.d('通知已开启');
|
||||||
} else if (status.isPermanentlyDenied) {
|
} else if (status.isPermanentlyDenied) {
|
||||||
Logger.d('通知权限已被拒绝,请到系统设置中开启');
|
Logger.d('通知权限已被拒绝,请到系统设置中开启');
|
||||||
} else if (status.isDenied) {
|
|
||||||
Logger.d('请授予通知权限,以便接收加氢站通知');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///推送相关
|
///推送相关初始化 (保持原样)
|
||||||
Future<void> initAliyunPush() async {
|
Future<void> initAliyunPush() async {
|
||||||
// 1. 配置分离:建议将 Key 提取到外部或配置文件中
|
|
||||||
final String appKey = Platform.isIOS ? AppTheme.ios_key : AppTheme.android_key;
|
final String appKey = Platform.isIOS ? AppTheme.ios_key : AppTheme.android_key;
|
||||||
final String appSecret = Platform.isIOS
|
final String appSecret = Platform.isIOS
|
||||||
? AppTheme.ios_appsecret
|
? AppTheme.ios_appsecret
|
||||||
: AppTheme.android_appsecret;
|
: AppTheme.android_appsecret;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 初始化推送
|
|
||||||
final result = await _aliyunPush.initPush(appKey: appKey, appSecret: appSecret);
|
final result = await _aliyunPush.initPush(appKey: appKey, appSecret: appSecret);
|
||||||
|
if (result['code'] != kAliyunPushSuccessCode) return;
|
||||||
if (result['code'] != kAliyunPushSuccessCode) {
|
|
||||||
Logger.d('初始化推送失败: ${result['code']} - ${result['errorMsg']}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.d('阿里云推送初始化成功');
|
|
||||||
// 分平台配置
|
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
await _setupIOSConfig();
|
await _setupIOSConfig();
|
||||||
} else if (Platform.isAndroid) {
|
} else if (Platform.isAndroid) {
|
||||||
await _setupAndroidConfig();
|
await _setupAndroidConfig();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.d('初始化过程中发生异常: $e');
|
Logger.d('初始化异常: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// iOS 专属配置
|
|
||||||
Future<void> _setupIOSConfig() async {
|
Future<void> _setupIOSConfig() async {
|
||||||
final res = await _aliyunPush.showIOSNoticeWhenForeground(true);
|
await _aliyunPush.showIOSNoticeWhenForeground(true);
|
||||||
if (res['code'] == kAliyunPushSuccessCode) {
|
|
||||||
Logger.d('iOS 前台通知展示已开启');
|
|
||||||
} else {
|
|
||||||
Logger.d('iOS 前台通知开启失败: ${res['errorMsg']}');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Android 专属配置
|
|
||||||
Future<void> _setupAndroidConfig() async {
|
Future<void> _setupAndroidConfig() async {
|
||||||
await _aliyunPush.setNotificationInGroup(true);
|
await _aliyunPush.setNotificationInGroup(true);
|
||||||
final res = await _aliyunPush.createAndroidChannel(
|
await _aliyunPush.createAndroidChannel(
|
||||||
"xll_push_android",
|
"xll_push_android",
|
||||||
'新消息通知',
|
'新消息通知',
|
||||||
4,
|
4,
|
||||||
'用于接收加氢站实时状态提醒',
|
'用于接收加氢站实时状态提醒',
|
||||||
);
|
);
|
||||||
if (res['code'] == kAliyunPushSuccessCode) {
|
|
||||||
Logger.d('Android 通知通道创建成功');
|
|
||||||
} else {
|
|
||||||
Logger.d('Android 通道创建失败: ${res['code']} - ${res['errorMsg']}');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPushCallback() {
|
void addPushCallback() {
|
||||||
@@ -139,40 +234,23 @@ class HomeController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
Future<void> _onAndroidNotificationClickedWithNoAction(
|
Future<void> _onAndroidNotificationClickedWithNoAction(
|
||||||
Map<dynamic, dynamic> message,
|
Map<dynamic, dynamic> message,
|
||||||
) async {
|
) async {}
|
||||||
Logger.d('onAndroidNotificationClickedWithNoAction ====> $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onAndroidNotificationReceivedInApp(Map<dynamic, dynamic> message) async {
|
Future<void> _onAndroidNotificationReceivedInApp(Map<dynamic, dynamic> message) async {}
|
||||||
Logger.d('onAndroidNotificationReceivedInApp ====> $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onMessage(Map<dynamic, dynamic> message) async {
|
Future<void> _onMessage(Map<dynamic, dynamic> message) async {}
|
||||||
Logger.d('onMessage ====> $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onNotification(Map<dynamic, dynamic> message) async {
|
Future<void> _onNotification(Map<dynamic, dynamic> message) async {}
|
||||||
Logger.d('onNotification ====> $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onNotificationOpened(Map<dynamic, dynamic> message) async {
|
Future<void> _onNotificationOpened(Map<dynamic, dynamic> message) async {
|
||||||
Logger.d('onNotificationOpened ====> $message');
|
|
||||||
await Get.to(() => const MessagePage());
|
await Get.to(() => const MessagePage());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onNotificationRemoved(Map<dynamic, dynamic> message) async {
|
Future<void> _onNotificationRemoved(Map<dynamic, dynamic> message) async {}
|
||||||
Logger.d('onNotificationRemoved ====> $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onIOSChannelOpened(Map<dynamic, dynamic> message) async {
|
Future<void> _onIOSChannelOpened(Map<dynamic, dynamic> message) async {}
|
||||||
Logger.d('onIOSChannelOpened ====> $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onIOSRegisterDeviceTokenSuccess(Map<dynamic, dynamic> message) async {
|
Future<void> _onIOSRegisterDeviceTokenSuccess(Map<dynamic, dynamic> message) async {}
|
||||||
Logger.d('onIOSRegisterDeviceTokenSuccess ====> $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onIOSRegisterDeviceTokenFailed(Map<dynamic, dynamic> message) async {
|
Future<void> _onIOSRegisterDeviceTokenFailed(Map<dynamic, dynamic> message) async {}
|
||||||
Logger.d('onIOSRegisterDeviceTokenFailed====> $message');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,13 @@ import 'package:ln_jq_app/pages/home/controller.dart';
|
|||||||
class HomePage extends GetView<HomeController> {
|
class HomePage extends GetView<HomeController> {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
|
|
||||||
// 主视图
|
|
||||||
Widget _buildView() {
|
|
||||||
return <Widget>[Text('主页面')].toColumn(mainAxisSize: MainAxisSize.min).center();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<HomeController>(
|
return GetBuilder<HomeController>(
|
||||||
init: HomeController(),
|
init: HomeController(),
|
||||||
id: 'home',
|
id: 'home',
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return controller.getHomePage();
|
return Scaffold(body: controller.getHomePage());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:ln_jq_app/common/login_util.dart';
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
||||||
@@ -17,6 +18,8 @@ import 'package:ln_jq_app/pages/login/controller.dart';
|
|||||||
import 'package:ln_jq_app/pages/url_host/view.dart';
|
import 'package:ln_jq_app/pages/url_host/view.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
|
||||||
|
import '../c_page/message/view.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
const LoginPage({super.key});
|
const LoginPage({super.key});
|
||||||
|
|
||||||
@@ -30,6 +33,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
bool _obscureText = true;
|
bool _obscureText = true;
|
||||||
bool _rememberPassword = true;
|
bool _rememberPassword = true;
|
||||||
bool _credentialsLoaded = false;
|
bool _credentialsLoaded = false;
|
||||||
|
bool isPushInitialized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -136,9 +140,10 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
// 根据 Tab 显示不同的输入框
|
// 根据 Tab 显示不同的输入框
|
||||||
_tabController.index == 0
|
if (_isAgreed)
|
||||||
|
(_tabController.index == 0
|
||||||
? _buildDriverInputFields(controller)
|
? _buildDriverInputFields(controller)
|
||||||
: _buildStationInputFields(controller),
|
: _buildStationInputFields(controller)),
|
||||||
|
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
// 统一登录按钮
|
// 统一登录按钮
|
||||||
@@ -146,7 +151,8 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
|
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
buildAgreement(),
|
buildAgreement(),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 80),
|
||||||
|
_buildFooterSlogan(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -155,7 +161,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(left: 0, right: 0, bottom: 33.h, child: _buildFooterSlogan()),
|
|
||||||
if (AppTheme.is_show_host)
|
if (AppTheme.is_show_host)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 40.h,
|
top: 40.h,
|
||||||
@@ -204,9 +209,9 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
"欢迎使用 ",
|
"欢迎使用小羚羚 ",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color.fromRGBO(51, 51, 51, 1),
|
color: Color.fromRGBO(51, 51, 51, 1),
|
||||||
),
|
),
|
||||||
@@ -241,10 +246,12 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller.phoneController,
|
controller: controller.phoneController,
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
|
maxLength: 11,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: '请输入手机号',
|
hintText: '请输入手机号',
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
counterText: ""
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -388,13 +395,28 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
content: _buildDialogContent(),
|
content: _buildDialogContent(),
|
||||||
confirmText: '同意',
|
confirmText: '同意',
|
||||||
cancelText: '拒绝',
|
cancelText: '拒绝',
|
||||||
onConfirm: () {
|
onConfirm: () async {
|
||||||
_isAgreed = true;
|
_isAgreed = true;
|
||||||
controller.updateUi();
|
controller.updateUi();
|
||||||
|
|
||||||
|
// 保存隐私政策同意状态
|
||||||
|
await StorageService.to.savePrivacyAgreed(true);
|
||||||
|
|
||||||
|
// 申请通知权限
|
||||||
|
await _requestNotificationPermission();
|
||||||
|
|
||||||
|
// 初始化阿里云推送
|
||||||
|
await _initPushService();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果已经同意过,但推送还没初始化,则初始化
|
||||||
|
if (!isPushInitialized) {
|
||||||
|
await _initPushService();
|
||||||
|
}
|
||||||
|
|
||||||
_tabController.index == 0
|
_tabController.index == 0
|
||||||
? _handleDriverLogin(controller)
|
? _handleDriverLogin(controller)
|
||||||
: _handleStationLogin(controller);
|
: _handleStationLogin(controller);
|
||||||
@@ -456,7 +478,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_processLoginResponse(responseData, "station", account);
|
_processLoginResponse(responseData, "station", account);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
@@ -520,7 +541,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
Logger.d("暂时不处理 查询车辆信息失败的情况");
|
Logger.d("暂时不处理 查询车辆信息失败的情况");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
Get.offAll(() => BaseWidgetsPage());
|
Get.offAll(() => BaseWidgetsPage());
|
||||||
} else {
|
} else {
|
||||||
@@ -536,6 +556,60 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
addAlias(identifier);
|
addAlias(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 申请通知权限
|
||||||
|
Future<void> _requestNotificationPermission() async {
|
||||||
|
final PermissionStatus status = await Permission.notification.request();
|
||||||
|
if (status.isGranted) {
|
||||||
|
Logger.d('通知权限已授予');
|
||||||
|
} else if (status.isPermanentlyDenied) {
|
||||||
|
Logger.d('通知权限被永久拒绝');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化推送服务
|
||||||
|
Future<void> _initPushService() async {
|
||||||
|
try {
|
||||||
|
final _aliyunPush = AliyunPushFlutter();
|
||||||
|
|
||||||
|
// 初始化推送
|
||||||
|
final String appKey = Platform.isIOS ? AppTheme.ios_key : AppTheme.android_key;
|
||||||
|
final String appSecret = Platform.isIOS
|
||||||
|
? AppTheme.ios_appsecret
|
||||||
|
: AppTheme.android_appsecret;
|
||||||
|
|
||||||
|
final result = await _aliyunPush.initPush(appKey: appKey, appSecret: appSecret);
|
||||||
|
if (result['code'] != kAliyunPushSuccessCode) {
|
||||||
|
Logger.d('推送初始化失败: ${result['errorMsg']}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置平台特定设置
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
await _aliyunPush.showIOSNoticeWhenForeground(true);
|
||||||
|
} else if (Platform.isAndroid) {
|
||||||
|
await _aliyunPush.setNotificationInGroup(true);
|
||||||
|
await _aliyunPush.createAndroidChannel(
|
||||||
|
"xll_push_android",
|
||||||
|
'新消息通知',
|
||||||
|
4,
|
||||||
|
'用于接收加氢站实时状态提醒',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加推送回调
|
||||||
|
_aliyunPush.addMessageReceiver(onNotificationOpened: _onNotificationOpened);
|
||||||
|
|
||||||
|
isPushInitialized = true;
|
||||||
|
Logger.d('推送服务初始化成功');
|
||||||
|
} catch (e) {
|
||||||
|
Logger.d('推送服务初始化异常: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onNotificationOpened(Map<dynamic, dynamic> message) async {
|
||||||
|
await Get.to(() => const MessagePage());
|
||||||
|
}
|
||||||
|
|
||||||
final _aliyunPush = AliyunPushFlutter();
|
final _aliyunPush = AliyunPushFlutter();
|
||||||
|
|
||||||
void addAlias(String alias) async {
|
void addAlias(String alias) async {
|
||||||
|
|||||||
@@ -1,16 +1,103 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:getx_scaffold/common/components/index.dart';
|
||||||
|
import 'package:getx_scaffold/common/widgets/rich_text_x.dart';
|
||||||
import 'package:ln_jq_app/pages/home/view.dart';
|
import 'package:ln_jq_app/pages/home/view.dart';
|
||||||
import 'package:ln_jq_app/pages/login/view.dart';
|
import 'package:ln_jq_app/pages/login/view.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
|
||||||
|
import '../common/webview/view.dart';
|
||||||
|
|
||||||
class WelcomeController extends GetxController {
|
class WelcomeController extends GetxController {
|
||||||
@override
|
@override
|
||||||
void onReady() {
|
void onReady() {
|
||||||
super.onReady();
|
super.onReady();
|
||||||
// 移除原生闪屏页(如果有的话)
|
// 移除原生闪屏页(如果有的话)
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
if (StorageService.to.isPrivacyAgreed) {
|
||||||
_startTimer();
|
_startTimer();
|
||||||
|
} else {
|
||||||
|
showPrivacyDialog();
|
||||||
|
}
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
_startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showPrivacyDialog() {
|
||||||
|
DialogX.to.showConfirmDialog(
|
||||||
|
title: "个人信息保护提示",
|
||||||
|
content: _buildDialogContent(),
|
||||||
|
confirmText: '同意',
|
||||||
|
cancelText: '不同意',
|
||||||
|
onConfirm: () async {
|
||||||
|
await StorageService.to.savePrivacyAgreed(true);
|
||||||
|
Get.offAll(() => const HomePage());
|
||||||
|
},
|
||||||
|
onCancel: () {
|
||||||
|
DialogX.to.showConfirmDialog(
|
||||||
|
title: "温馨提示",
|
||||||
|
content: RichTextX(
|
||||||
|
children: [
|
||||||
|
TextSpanItem('如果您不同意'),
|
||||||
|
TextSpanItem(
|
||||||
|
'《隐私协议》',
|
||||||
|
onTap: () => openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html"),
|
||||||
|
),
|
||||||
|
TextSpanItem('和'),
|
||||||
|
TextSpanItem(
|
||||||
|
'《用户政策》',
|
||||||
|
onTap: () => openPage("用户协议", "https://lnh2e.com/user_agreement.html"),
|
||||||
|
),
|
||||||
|
TextSpanItem(
|
||||||
|
',很遗憾我们将无法为您提供服务。您需要同意以上协议后,才能使用本应用。\n\n我们将严格按照相关法律法规要求,坚决保护您的个人隐私和信息安全。',
|
||||||
|
),
|
||||||
|
TextSpanItem('\n点击“同意”按钮,表示您已知情并同意以上协议。'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
confirmText: '同意并继续',
|
||||||
|
cancelText: '不同意',
|
||||||
|
onConfirm: () async {
|
||||||
|
await StorageService.to.savePrivacyAgreed(true);
|
||||||
|
Get.offAll(() => const HomePage());
|
||||||
|
},
|
||||||
|
onCancel: () {
|
||||||
|
SystemNavigator.pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDialogContent() {
|
||||||
|
return RichTextX(
|
||||||
|
children: [
|
||||||
|
TextSpanItem('欢迎使用小羚羚!\n我们将通过'),
|
||||||
|
TextSpanItem(
|
||||||
|
'《隐私协议》',
|
||||||
|
onTap: () => openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html"),
|
||||||
|
),
|
||||||
|
TextSpanItem('和'),
|
||||||
|
TextSpanItem(
|
||||||
|
'《用户政策》',
|
||||||
|
onTap: () => openPage("用户协议", "https://lnh2e.com/user_agreement.html"),
|
||||||
|
),
|
||||||
|
TextSpanItem(
|
||||||
|
',帮助您了解我们为您提供的服务、我们如何处理个人信息以及您享有的权利。我们会严格按照相关法律法规要求,采取各种安全措施来保护您的个人信息。',
|
||||||
|
),
|
||||||
|
TextSpanItem('\n点击“同意”按钮,表示您已知情并同意以上协议。'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void openPage(String title, String url) {
|
||||||
|
Get.to(() => const WebViewPage(), arguments: {'title': title, 'url': url});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startTimer() {
|
void _startTimer() {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class WelcomePage extends GetView<WelcomeController> {
|
|||||||
right: 0,
|
right: 0,
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/welcome.png',
|
'assets/images/welcome.png',
|
||||||
fit: BoxFit.fill
|
fit: BoxFit.cover
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -26,11 +26,14 @@ class StorageService extends GetxService {
|
|||||||
static const String _stationAccountKey = 'station_account';
|
static const String _stationAccountKey = 'station_account';
|
||||||
static const String _stationPasswordKey = 'station_password';
|
static const String _stationPasswordKey = 'station_password';
|
||||||
|
|
||||||
// 新增:用于标记“绑定车辆”弹窗是否已在本会话中显示过
|
// 新增:用于标记”绑定车辆”弹窗是否已在本会话中显示过
|
||||||
static const String _bindDialogShownKey = 'bind_vehicle_dialog_shown';
|
static const String _bindDialogShownKey = 'bind_vehicle_dialog_shown';
|
||||||
|
|
||||||
static const String _hostUrlKey = 'host_url';
|
static const String _hostUrlKey = 'host_url';
|
||||||
|
|
||||||
|
// 隐私政策相关
|
||||||
|
static const String _privacyAgreedKey = 'privacy_agreed';
|
||||||
|
|
||||||
static StorageService get to => Get.find();
|
static StorageService get to => Get.find();
|
||||||
|
|
||||||
Future<StorageService> init() async {
|
Future<StorageService> init() async {
|
||||||
@@ -64,9 +67,12 @@ class StorageService extends GetxService {
|
|||||||
|
|
||||||
String? get stationPassword => _box.read<String?>(_stationPasswordKey);
|
String? get stationPassword => _box.read<String?>(_stationPasswordKey);
|
||||||
|
|
||||||
// 新增:获取“绑定车辆”弹窗是否已显示的标志
|
// 新增:获取”绑定车辆”弹窗是否已显示的标志
|
||||||
bool get hasShownBindVehicleDialog => _box.read<bool>(_bindDialogShownKey) ?? false;
|
bool get hasShownBindVehicleDialog => _box.read<bool>(_bindDialogShownKey) ?? false;
|
||||||
|
|
||||||
|
// 获取隐私政策是否已同意
|
||||||
|
bool get isPrivacyAgreed => _box.read<bool>(_privacyAgreedKey) ?? false;
|
||||||
|
|
||||||
VehicleInfo? get vehicleInfo {
|
VehicleInfo? get vehicleInfo {
|
||||||
final vehicleJson = _box.read<String?>(_vehicleInfoKey);
|
final vehicleJson = _box.read<String?>(_vehicleInfoKey);
|
||||||
if (vehicleJson != null) {
|
if (vehicleJson != null) {
|
||||||
@@ -117,6 +123,11 @@ class StorageService extends GetxService {
|
|||||||
await _box.write(_stationPasswordKey, password);
|
await _box.write(_stationPasswordKey, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存隐私政策同意状态
|
||||||
|
Future<void> savePrivacyAgreed(bool agreed) async {
|
||||||
|
await _box.write(_privacyAgreedKey, agreed);
|
||||||
|
}
|
||||||
|
|
||||||
// 新增:标记“绑定车辆”弹窗已显示
|
// 新增:标记“绑定车辆”弹窗已显示
|
||||||
Future<void> markBindVehicleDialogAsShown() async {
|
Future<void> markBindVehicleDialogAsShown() async {
|
||||||
await _box.write(_bindDialogShownKey, true);
|
await _box.write(_bindDialogShownKey, true);
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.2.3+6
|
version: 1.2.5+8
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.0
|
sdk: ^3.9.0
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
# Dependencies specify other packages tha。。。t your package needs in order to work.
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
# dependencies can be manually updated by changing the version numbers below to
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
@@ -46,15 +46,15 @@ dependencies:
|
|||||||
dropdown_button2: ^2.3.8
|
dropdown_button2: ^2.3.8
|
||||||
image_picker: ^1.2.1 # 用于从相册选择图片
|
image_picker: ^1.2.1 # 用于从相册选择图片
|
||||||
image: ^4.5.4
|
image: ^4.5.4
|
||||||
mobile_scanner: ^7.1.4
|
|
||||||
flutter_pdfview: 1.4.3 #显示pdf
|
flutter_pdfview: 1.4.3 #显示pdf
|
||||||
photo_view: ^0.15.0 #操作图片
|
photo_view: ^0.15.0 #操作图片
|
||||||
flutter_inappwebview: ^6.1.5 # WebView插件
|
flutter_inappwebview: ^6.1.5 # WebView插件
|
||||||
geolocator: ^14.0.2 # 获取精确定位
|
geolocator: ^14.0.2 # 获取精确定位
|
||||||
aliyun_push_flutter: ^1.3.6
|
aliyun_push_flutter: ^1.3.6
|
||||||
pull_to_refresh: ^2.0.0
|
pull_to_refresh: ^2.0.0
|
||||||
|
flutter_app_update: ^3.2.2
|
||||||
|
saver_gallery: ^4.0.0
|
||||||
|
path_provider: ^2.1.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -65,6 +65,7 @@ dev_dependencies:
|
|||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
intl: 0.19.0
|
intl: 0.19.0
|
||||||
|
device_info_plus: ^12.3.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
|
|||||||