4 Commits

Author SHA1 Message Date
cd14469d79 调整定位 2026-03-12 13:27:08 +08:00
b846d352a2 android 地图 2026-03-10 17:31:27 +08:00
xiaogg
a24f41a8d5 fix:添加本地库 2026-03-09 09:38:22 +08:00
xiaogg
65b4a3ac34 接入地图SDK 2026-03-09 08:53:34 +08:00
92 changed files with 4812 additions and 4199 deletions

View File

@@ -37,8 +37,8 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = 7
versionName = "1.2.4"
versionCode = 6
versionName = "1.2.3"
}
signingConfigs {
@@ -68,3 +68,8 @@ android {
flutter {
source = "../.."
}
dependencies {
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")
}

View File

@@ -1,50 +1,65 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<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" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<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_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--用于申请调用A-GPS模块-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"></uses-permission>
<!--如果设置了target >= 28 如果需要启动后台定位则必须声明这个权限-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!--如果您的应用需要后台定位权限且有可能运行在Android Q设备上,并且设置了target>28必须增加这个权限声明-->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:requestLegacyExternalStorage="true"
android:label="小羚羚"
android:name="${applicationName}"
android:icon="@mipmap/logo"
android:label="小羚羚">
android:icon="@mipmap/logo">
<!-- 高德地图Key -->
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="92495660f7bc990cb475426c47c03b65" />
<!-- 高德地图定位服务 -->
<service android:name="com.amap.api.location.APSService" />
<!--高德导航-->
<activity
android:name="com.amap.api.navi.AmapRouteActivity"
android:theme="@android:style/Theme.NoTitleBar"
android:configChanges="orientation|keyboardHidden|screenSize|navigation" />
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
@@ -54,13 +69,32 @@
android:value="2" />
<!-- 请填写你自己的- appKey -->
<meta-data
android:name="com.alibaba.app.appkey"
android:value="335642645" />
<meta-data android:name="com.alibaba.app.appkey" android:value="335642645"/>
<!-- 请填写你自己的appSecret -->
<meta-data
android:name="com.alibaba.app.appsecret"
android:value="39628204345a4240b5b645b68a5896c7" />
<meta-data android:name="com.alibaba.app.appsecret" android:value="39628204345a4240b5b645b68a5896c7"/>
<!-- 华为通道的参数appid -->
<meta-data android:name="com.huawei.hms.client.appid" android:value="" />
<!-- vivo通道的参数api_key为appkey -->
<meta-data android:name="com.vivo.push.api_key" android:value="" />
<meta-data android:name="com.vivo.push.app_id" android:value="" />
<!-- honor通道的参数-->
<meta-data android:name="com.hihonor.push.app_id" android:value="" />
<!-- oppo -->
<meta-data android:name="com.oppo.push.key" android:value="" />
<meta-data android:name="com.oppo.push.secret" android:value="" />
<!-- 小米-->
<meta-data android:name="com.xiaomi.push.id" android:value="id=2222222222222222222" />
<meta-data android:name="com.xiaomi.push.key" android:value="id=5555555555555" />
<!-- 魅族-->
<meta-data android:name="com.meizu.push.id" android:value="" />
<meta-data android:name="com.meizu.push.key" android:value="" />
<!-- 接收推送消息 -->
<receiver
@@ -83,7 +117,6 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -102,8 +135,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -1,6 +1,195 @@
package com.lnkj.ln_jq_app;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.lnkj.ln_jq_app/map";
private static final String TAG = "MainActivity";
// 权限请求码
private static final int PERMISSION_REQUEST_CODE = 1001;
private NativeMapView mapView;
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
// 注册高德地图导航Platform View
flutterEngine
.getPlatformViewsController()
.getRegistry()
.registerViewFactory(
"NativeFirstPage",
new NativeMapFactory(this)
);
// 注册方法通道用于地图控制
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler((call, result) -> {
switch (call.method) {
case "requestPermissions":
requestPermissions();
result.success(null);
break;
case "onResume":
if (mapView != null) {
mapView.onResume();
}
result.success(null);
break;
case "onPause":
if (mapView != null) {
mapView.onPause();
}
result.success(null);
break;
case "onDestroy":
if (mapView != null) {
mapView.dispose();
mapView = null;
}
result.success(null);
break;
default:
result.notImplemented();
break;
}
});
}
/**
* 获取当前系统版本需要申请的权限列表
*/
private String[] getRequiredPermissions() {
List<String> permissions = new ArrayList<>();
// 定位权限是必须的
permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
// 存储权限处理Android 13 (API 33) 以下才需要申请 legacy 存储权限
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
}
return permissions.toArray(new String[0]);
}
/**
* 检查并申请权限
*/
private void checkAndRequestPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String[] requiredPermissions = getRequiredPermissions();
List<String> deniedPermissions = new ArrayList<>();
for (String permission : requiredPermissions) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
deniedPermissions.add(permission);
}
}
if (!deniedPermissions.isEmpty()) {
ActivityCompat.requestPermissions(
this,
deniedPermissions.toArray(new String[0]),
PERMISSION_REQUEST_CODE
);
} else {
Log.d(TAG, "所有必要权限已授予");
if (mapView != null) {
mapView.startLocation();
}
}
} else {
if (mapView != null) {
mapView.startLocation();
}
}
}
private void requestPermissions() {
checkAndRequestPermissions();
}
public void setMapView(NativeMapView mapView) {
this.mapView = mapView;
}
@Override
protected void onResume() {
super.onResume();
// 注意高德SDK合规检查通过后再进行定位相关操作
// 这里仅保留地图生命周期调用权限建议在Flutter端或按需触发
if (mapView != null) {
mapView.onResume();
}
}
@Override
protected void onPause() {
super.onPause();
if (mapView != null) {
mapView.onPause();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mapView != null) {
mapView.dispose();
mapView = null;
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (mapView != null) {
mapView.onSaveInstanceState(outState);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
boolean locationGranted = false;
for (int i = 0; i < permissions.length; i++) {
if (Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[i])
&& grantResults[i] == PackageManager.PERMISSION_GRANTED) {
locationGranted = true;
break;
}
}
if (locationGranted) {
if (mapView != null) {
mapView.startLocation();
}
} else {
// 只有在定位权限确实被拒绝时才弹出提示
Toast.makeText(this, "请授予应用定位权限以正常使用地图功能", Toast.LENGTH_LONG).show();
}
}
}
}

View File

@@ -0,0 +1,37 @@
package com.lnkj.ln_jq_app;
import android.content.Context;
import io.flutter.plugin.common.MessageCodec;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
/**
* 高德地图导航 Platform View Factory
* 对应iOS的NativeViewFactory
*/
public class NativeMapFactory extends PlatformViewFactory {
private static final String VIEW_TYPE_ID = "NativeFirstPage";
private static NativeMapView mapViewInstance = null;
private final Context context;
public NativeMapFactory(Context context) {
super(StandardMessageCodec.INSTANCE);
this.context = context;
}
@Override
public PlatformView create(Context context, int viewId, Object args) {
mapViewInstance = new NativeMapView(context, viewId, args);
return mapViewInstance;
}
/**
* 获取地图实例供MainActivity使用
*/
public static NativeMapView getMapView() {
return mapViewInstance;
}
}

View File

@@ -0,0 +1,695 @@
package com.lnkj.ln_jq_app;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.amap.api.location.AMapLocation;
import com.amap.api.location.AMapLocationClient;
import com.amap.api.location.AMapLocationClientOption;
import com.amap.api.location.AMapLocationListener;
import com.amap.api.maps.AMap;
import com.amap.api.maps.AMapOptions;
import com.amap.api.maps.CameraUpdateFactory;
import com.amap.api.maps.LocationSource;
import com.amap.api.maps.MapView;
import com.amap.api.maps.MapsInitializer;
import com.amap.api.maps.model.BitmapDescriptorFactory;
import com.amap.api.maps.model.LatLng;
import com.amap.api.maps.model.Marker;
import com.amap.api.maps.model.MarkerOptions;
import com.amap.api.maps.model.MyLocationStyle;
import com.amap.api.maps.model.Poi;
import com.amap.api.navi.AMapNavi;
import com.amap.api.navi.AmapNaviPage;
import com.amap.api.navi.AmapNaviParams;
import com.amap.api.navi.AmapNaviType;
import com.amap.api.navi.AmapPageType;
import com.amap.api.navi.INaviInfoCallback;
import com.amap.api.navi.enums.PathPlanningStrategy;
import com.amap.api.navi.model.AMapCarInfo;
import com.amap.api.navi.model.AMapNaviLocation;
import com.amap.api.services.core.AMapException;
import com.amap.api.services.core.LatLonPoint;
import com.amap.api.services.geocoder.GeocodeResult;
import com.amap.api.services.geocoder.GeocodeSearch;
import com.amap.api.services.geocoder.RegeocodeAddress;
import com.amap.api.services.geocoder.RegeocodeQuery;
import com.amap.api.services.geocoder.RegeocodeResult;
import com.amap.api.services.route.BusRouteResult;
import com.amap.api.services.route.DriveRouteResult;
import com.amap.api.services.route.RideRouteResult;
import com.amap.api.services.route.RouteSearch;
import com.amap.api.services.route.WalkRouteResult;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import io.flutter.plugin.platform.PlatformView;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
/**
* 高德地图导航
*/
public class NativeMapView implements PlatformView, LocationSource, AMapLocationListener,
GeocodeSearch.OnGeocodeSearchListener, RouteSearch.OnRouteSearchListener, AMap.OnMarkerClickListener {
private static final String TAG = "NativeMapView";
private final FrameLayout container;
// 地图相关
private MapView mapView;
private AMap aMap;
private OnLocationChangedListener mListener;
// 定位相关
private AMapLocationClient mlocationClient;
private final Context mContext;
private Activity mActivity; // 保存Activity引用用于导航
private GeocodeSearch geocoderSearch;
private RouteSearch routeSearch;
private final OkHttpClient httpClient = new OkHttpClient();
// UI组件
private EditText startInput;
private EditText endInput;
private LatLng currentLatLng;
private String startName = "我的位置";
private String endName = "目的地";
private LatLng startPoint;
private LatLng endPoint;
private boolean isFirstLocation = true;
private final List<Marker> stationMarkers = new ArrayList<>();
public NativeMapView(Context context, int id, Object args) {
this.mContext = context;
// 尝试获取Activity引用
mActivity = getActivityFromContext(context);
MapsInitializer.updatePrivacyShow(context, true, true);
MapsInitializer.updatePrivacyAgree(context, true);
container = new FrameLayout(context);
container.setClickable(true);
container.setFocusable(true);
mapView = new MapView(context);
mapView.onCreate(null);
aMap = mapView.getMap();
container.addView(mapView, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
initServices(context);
initOverlays(context);
setupMapUi();
// 通知MainActivity
if (context instanceof MainActivity) {
((MainActivity) context).setMapView(this);
}
Log.d(TAG, "NativeMapView初始化完成");
}
/**
* 初始化服务
*/
private void initServices(Context context) {
try {
geocoderSearch = new GeocodeSearch(context);
geocoderSearch.setOnGeocodeSearchListener(this);
routeSearch = new RouteSearch(context);
routeSearch.setRouteSearchListener(this);
} catch (AMapException e) {
Log.e(TAG, "服务初始化失败", e);
}
}
/**
* 初始化覆盖层UI
*/
private void initOverlays(Context context) {
LinearLayout searchBox = new LinearLayout(context);
searchBox.setOrientation(LinearLayout.VERTICAL);
searchBox.setBackground(getRoundedDrawable(Color.WHITE, 12));
searchBox.setElevation(dp2px(8));
int p = dp2px(15);
searchBox.setPadding(p, p, p, p);
searchBox.setClickable(true);
searchBox.setFocusable(true);
startInput = createInput(context, "起点: 正在定位...");
searchBox.addView(startInput);
View vSpace = new View(context);
searchBox.addView(vSpace, new LinearLayout.LayoutParams(1, dp2px(10)));
LinearLayout endRow = new LinearLayout(context);
endRow.setOrientation(LinearLayout.HORIZONTAL);
endRow.setGravity(Gravity.CENTER_VERTICAL);
endInput = createInput(context, "终点: 请输入目的地");
endRow.addView(endInput, new LinearLayout.LayoutParams(0, dp2px(42), 1f));
Button routeBtn = new Button(context);
routeBtn.setText("路径规划");
routeBtn.setTextColor(Color.WHITE);
routeBtn.setAllCaps(false);
routeBtn.setTextSize(14);
routeBtn.setBackground(getRoundedDrawable(Color.parseColor("#017143"), 6));
routeBtn.setOnClickListener(v -> startRouteSearch());
endRow.addView(routeBtn, new LinearLayout.LayoutParams(dp2px(90), dp2px(42)));
searchBox.addView(endRow);
FrameLayout.LayoutParams searchParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
searchParams.setMargins(dp2px(15), dp2px(50), dp2px(15), 0);
container.addView(searchBox, searchParams);
ImageButton locBtn = new ImageButton(context);
locBtn.setImageResource(android.R.drawable.ic_menu_mylocation);
locBtn.setBackground(getRoundedDrawable(Color.WHITE, 30));
locBtn.setElevation(dp2px(4));
locBtn.setOnClickListener(v -> {
if (currentLatLng != null) {
aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(currentLatLng, 15f));
}
});
FrameLayout.LayoutParams locParams = new FrameLayout.LayoutParams(dp2px(50), dp2px(50));
locParams.setMargins(0, 0, dp2px(20), dp2px(120));
locParams.gravity = Gravity.BOTTOM | Gravity.END;
container.addView(locBtn, locParams);
}
private EditText createInput(Context context, String hint) {
EditText et = new EditText(context);
et.setHint(hint);
et.setTextSize(14f);
et.setTextColor(Color.BLACK);
et.setPadding(dp2px(12), dp2px(10), dp2px(12), dp2px(10));
et.setBackground(getRoundedDrawable(Color.parseColor("#F5F5F5"), 6));
et.setSingleLine(true);
et.setImeOptions(EditorInfo.IME_ACTION_DONE);
et.setFocusable(true);
et.setFocusableInTouchMode(true);
return et;
}
private GradientDrawable getRoundedDrawable(int color, int radiusDp) {
GradientDrawable shape = new GradientDrawable();
shape.setShape(GradientDrawable.RECTANGLE);
shape.setCornerRadius(dp2px(radiusDp));
shape.setColor(color);
return shape;
}
private int dp2px(float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mContext.getResources().getDisplayMetrics());
}
private void setupMapUi() {
aMap.setLocationSource(this);
aMap.setMyLocationEnabled(true);
aMap.setOnMarkerClickListener(this);
MyLocationStyle myLocationStyle = new MyLocationStyle();
// --- 放大定位图标 ---
try {
Bitmap carBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.car);
if (carBitmap != null) {
// 放大到 80dp
int iconSize = dp2px(25);
Bitmap scaledBitmap = Bitmap.createScaledBitmap(carBitmap, iconSize, iconSize, true);
myLocationStyle.myLocationIcon(BitmapDescriptorFactory.fromBitmap(scaledBitmap));
}
} catch (Exception e) {
Log.e(TAG, "设置大图标失败", e);
}
myLocationStyle.anchor(0.5f, 0.5f);
myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER);
myLocationStyle.showMyLocation(true);
myLocationStyle.strokeColor(Color.TRANSPARENT);
myLocationStyle.radiusFillColor(Color.TRANSPARENT);
aMap.setMyLocationStyle(myLocationStyle);
aMap.getUiSettings().setZoomControlsEnabled(true);
aMap.getUiSettings().setScaleControlsEnabled(true);
aMap.getUiSettings().setLogoPosition(AMapOptions.LOGO_POSITION_BOTTOM_LEFT);
}
// ==================== LocationSource 接口实现 ====================
@Override
public void activate(OnLocationChangedListener listener) {
mListener = listener;
startLocation();
}
@Override
public void deactivate() {
mListener = null;
if (mlocationClient != null) {
mlocationClient.stopLocation();
mlocationClient.onDestroy();
}
mlocationClient = null;
}
public void startLocation() {
if (mlocationClient == null) {
try {
mlocationClient = new AMapLocationClient(mContext);
AMapLocationClientOption option = new AMapLocationClientOption();
mlocationClient.setLocationListener(this);
option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
mlocationClient.setLocationOption(option);
mlocationClient.startLocation();
Log.d(TAG, "定位启动成功");
} catch (Exception e) {
Log.e(TAG, "定位启动失败", e);
}
}
}
@Override
public void onLocationChanged(AMapLocation loc) {
if (loc != null) {
if (mListener != null) {
mListener.onLocationChanged(loc);
}
currentLatLng = new LatLng(loc.getLatitude(), loc.getLongitude());
if (loc.getErrorCode() == 0) {
if (isFirstLocation) {
isFirstLocation = false;
startPoint = currentLatLng;
aMap.moveCamera(CameraUpdateFactory.newLatLngZoom(currentLatLng, 14f));
getAddressByLatlng(currentLatLng);
fetchRecommendStation(loc);
fetchNearbyStations(loc);
}
}
}
}
// ==================== 逆地理编码 ====================
private void getAddressByLatlng(LatLng latLng) {
LatLonPoint point = new LatLonPoint(latLng.latitude, latLng.longitude);
RegeocodeQuery query = new RegeocodeQuery(point, 200, GeocodeSearch.AMAP);
geocoderSearch.getFromLocationAsyn(query);
}
@Override
public void onRegeocodeSearched(RegeocodeResult result, int rCode) {
if (rCode == AMapException.CODE_AMAP_SUCCESS && result != null && result.getRegeocodeAddress() != null) {
RegeocodeAddress addr = result.getRegeocodeAddress();
String fullAddr = addr.getFormatAddress();
// 优化地址显示逻辑
startName = formatAddress(fullAddr,result);
new Handler(Looper.getMainLooper()).post(() -> {
startInput.setText(startName);
startInput.setSelection(startName.length()); // 光标移到末尾
});
Log.d(TAG, "逆地理编码成功: " + startName);
} else {
Log.e(TAG, "逆地理编码失败: code=" + rCode + ", result=" + (result != null ? "null" : "has data"));
}
}
/**
* 格式化地址显示,移除重复的前缀并限制长度
*/
private String formatAddress(String fullAddress,RegeocodeResult result) {
if (fullAddress == null || fullAddress.isEmpty()) {
return "未知地点";
}
// 获取各级地址信息
String province = null;
String city = null;
String district = null;
String township = null;
try {
if (result.getRegeocodeAddress() != null) {
RegeocodeAddress addr = result.getRegeocodeAddress();
province = addr.getProvince();
city = addr.getCity();
district = addr.getDistrict();
township = addr.getTownship();
}
} catch (Exception e) {
Log.e(TAG, "获取地址信息失败", e);
}
String formattedAddr = fullAddress;
// 按优先级移除重复前缀(省、市、区、乡)
String[] prefixes = {province, city, district, township};
for (String prefix : prefixes) {
if (prefix != null && !prefix.isEmpty() && formattedAddr.startsWith(prefix)) {
formattedAddr = formattedAddr.substring(prefix.length());
Log.d(TAG, "移除前缀: " + prefix + " -> " + formattedAddr);
}
}
// 限制地址长度并添加省略号
if (formattedAddr.length() > 25) {
formattedAddr = formattedAddr.substring(0, 25) + "...";
Log.d(TAG, "地址长度截断: " + formattedAddr);
}
return formattedAddr;
}
@Override
public void onGeocodeSearched(GeocodeResult result, int rCode) {
}
// ==================== API请求 ====================
private void fetchRecommendStation(AMapLocation loc) {
try {
JSONObject json = new JSONObject();
json.put("province", loc.getProvince() != null ? loc.getProvince() : "");
json.put("city", loc.getCity() != null && !loc.getCity().isEmpty() ? loc.getCity() : "");
json.put("district", loc.getDistrict() != null ? loc.getDistrict() : "");
json.put("longitude", String.valueOf(loc.getLongitude()));
json.put("latitude", String.valueOf(loc.getLatitude()));
RequestBody body = RequestBody.create(json.toString(), MediaType.parse("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url("https://beta-esg.api.lnh2e.com/appointment/station/getStationInfoByArea")
.post(body)
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (response.isSuccessful() && response.body() != null) {
try {
JSONObject res = new JSONObject(response.body().string());
if (res.getInt("code") == 0 && !res.isNull("data")) {
JSONObject data = res.getJSONObject("data");
endPoint = new LatLng(data.getDouble("latitude"), data.getDouble("longitude"));
endName = data.getString("name");
String addr = data.optString("address", "");
new Handler(Looper.getMainLooper()).post(() -> {
endInput.setText(addr);
markStation(endPoint, endName, true);
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void fetchNearbyStations(AMapLocation loc) {
try {
JSONObject json = new JSONObject();
json.put("longitude", String.valueOf(loc.getLongitude()));
json.put("latitude", String.valueOf(loc.getLatitude()));
RequestBody body = RequestBody.create(json.toString(), MediaType.parse("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url("https://beta-esg.api.lnh2e.com/appointment/station/getNearbyHydrogenStationsByLocation")
.post(body)
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (response.isSuccessful() && response.body() != null) {
try {
JSONObject res = new JSONObject(response.body().string());
if (res.getInt("code") == 0 && !res.isNull("data")) {
JSONArray array = res.getJSONArray("data");
new Handler(Looper.getMainLooper()).post(() -> {
for (int i = 0; i < array.length(); i++) {
try {
JSONObject item = array.getJSONObject(i);
markStation(new LatLng(item.getDouble("latitude"), item.getDouble("longitude")),
item.getString("name"), false);
} catch (Exception ignored) {
}
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void markStation(LatLng latLng, String name, boolean isRecommend) {
MarkerOptions opt = new MarkerOptions()
.position(latLng).title(name)
.icon(BitmapDescriptorFactory.defaultMarker(isRecommend ? BitmapDescriptorFactory.HUE_RED : BitmapDescriptorFactory.HUE_GREEN));
Marker m = aMap.addMarker(opt);
m.setObject(latLng);
stationMarkers.add(m);
}
@Override
public boolean onMarkerClick(Marker marker) {
if (marker.getObject() instanceof LatLng) {
endPoint = (LatLng) marker.getObject();
endName = marker.getTitle();
endInput.setText(endName);
startRouteSearch();
}
return true;
}
// ==================== 路径规划 ====================
private void startRouteSearch() {
if (startPoint == null || endPoint == null) {
Toast.makeText(mContext, "正在定位中,请稍后...", Toast.LENGTH_SHORT).show();
return;
}
Poi start = new Poi(startName, startPoint, "");
Poi end = new Poi(endName, endPoint, "");
AmapNaviParams params = new AmapNaviParams(start, null, end, AmapNaviType.DRIVER, AmapPageType.ROUTE);
try {
AMapNavi mAMapNavi = AMapNavi.getInstance(mContext);
AMapCarInfo carInfo = new AMapCarInfo();
carInfo.setCarNumber("沪AGK2267");
carInfo.setCarType("1");
carInfo.setVehicleAxis("6");
carInfo.setVehicleHeight("3.32");
carInfo.setVehicleLength("6.9");
carInfo.setVehicleWidth("2.26");
carInfo.setVehicleSize("2");
carInfo.setVehicleLoad("4.5");
carInfo.setVehicleWeight("4.5");
carInfo.setRestriction(true);
carInfo.setVehicleLoadSwitch(true);
mAMapNavi.setCarInfo(carInfo);
} catch (com.amap.api.maps.AMapException e) {
Log.e(TAG, "设置车辆信息失败", e);
}
params.setRouteStrategy(PathPlanningStrategy.DRIVING_MULTIPLE_ROUTES_DEFAULT);
if (mActivity != null) {
AmapNaviPage.getInstance().showRouteActivity(mActivity, params, new INaviInfoCallback() {
@Override
public void onInitNaviFailure() {
Log.e(TAG, "导航初始化失败");
}
@Override
public void onGetNavigationText(String s) {
}
@Override
public void onLocationChange(AMapNaviLocation location) {
}
@Override
public void onArriveDestination(boolean b) {
}
@Override
public void onStartNavi(int i) {
}
@Override
public void onCalculateRouteSuccess(int[] ints) {
}
@Override
public void onCalculateRouteFailure(int i) {
}
@Override
public void onStopSpeaking() {
}
@Override
public void onReCalculateRoute(int i) {
}
@Override
public void onArrivedWayPoint(int i) {
}
@Override
public void onExitPage(int i) {
}
@Override
public void onStrategyChanged(int i) {
}
@Override
public void onMapTypeChanged(int i) {
}
@Override
public void onNaviDirectionChanged(int i) {
}
@Override
public void onDayAndNightModeChanged(int i) {
}
@Override
public void onBroadcastModeChanged(int i) {
}
@Override
public void onScaleAutoChanged(boolean b) {
}
@Override
public View getCustomNaviBottomView() {
return null;
}
@Override
public View getCustomNaviView() {
return null;
}
@Override
public View getCustomMiddleView() {
return null;
}
});
}
}
@Override
public void onDriveRouteSearched(DriveRouteResult result, int rCode) {
}
@Override
public void onBusRouteSearched(BusRouteResult r, int c) {
}
@Override
public void onWalkRouteSearched(WalkRouteResult r, int c) {
}
@Override
public void onRideRouteSearched(RideRouteResult r, int c) {
}
private Activity getActivityFromContext(Context context) {
if (context instanceof Activity)
return (Activity) context;
if (context instanceof android.content.ContextWrapper) {
Context base = ((android.content.ContextWrapper) context).getBaseContext();
if (base instanceof Activity)
return (Activity) base;
}
return null;
}
public void onResume() {
mapView.onResume();
}
public void onPause() {
mapView.onPause();
}
public void onSaveInstanceState(Bundle out) {
mapView.onSaveInstanceState(out);
}
@Override
public View getView() {
return container;
}
@Override
public void dispose() {
if (mlocationClient != null) {
mlocationClient.stopLocation();
mlocationClient.onDestroy();
}
mapView.onDestroy();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -21,9 +21,11 @@
.amap-callamap,
.amap-lib-driving-callBtn,
.amap-copyright,
.amap-logo{bottom: 60px}
.amap-logo {
display: none !important;
}
/* 去除高德默认的 label 边框 and 背景 */
/* 去除高德默认的 label 边框背景 */
.amap-marker-label {
border: none !important;
background-color: transparent !important;
@@ -107,7 +109,7 @@
/* --- 导航结果面板 (底部弹出) --- */
#panel {
position: fixed;
bottom: 95px;
bottom: 75px;
left: 0;
width: 100%;
height: 35%;
@@ -127,7 +129,7 @@
#location-btn {
position: fixed;
right: 10px;
bottom: 105px;
bottom: 75px;
/* 默认位置 */
width: 44px;
height: 44px;
@@ -157,7 +159,7 @@
/* --- 调整比例尺位置 --- */
.amap-scalecontrol {
/* 初始状态:避开底部的定位按钮或留出安全间距 */
bottom: 110px !important;
bottom: 80px !important;
left: 10px !important;
transition: bottom 0.3s ease;
/* 增加平滑动画 */
@@ -193,10 +195,10 @@
<div id="search-box">
<div class="input-row">
<input id="startInput" placeholder="起点: 请输入当前地点" onfocus="this.select()" />
<input id="startInput" placeholder="起点: 请输入当前地点" />
</div>
<div class="input-row">
<input id="endInput" placeholder="终点: 请输入目的地" onfocus="this.select()" />
<input id="endInput" placeholder="终点: 请输入目的地" />
<button onclick="startRouteSearch()">路径规划</button>
</div>
</div>
@@ -219,7 +221,6 @@
var currentLat, currentLng;
var isTruckMode = false;
var isInitialLocationSet = false;
var stationMarkers = []; // 存储所有站点的标记
function initMap() {
@@ -242,11 +243,6 @@
}
});
// 点击地图空白处重置状态
map.on('click', function() {
resetSearchState();
});
// 添加基础控件
map.addControl(new AMap.Scale());
map.addControl(new AMap.ToolBar({
@@ -288,19 +284,6 @@
});
}
/**
* 重置搜索状态,隐藏面板和路线
*/
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 传来的定位数据
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
@@ -353,8 +336,6 @@
fetchStationInfo(addressComponent.province, addressComponent.city,
addressComponent.district, lat, lng);
fetchStationInfoList(lat, lng);
// 策略1: 优先使用最近的、类型合适的POI的名称
if (pois && pois.length > 0) {
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI这类POI通常是具体的建筑或地点名
@@ -416,6 +397,7 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
// "asoco-token": "e28eada8-4611-4dc2-a942-0122e52f52da"
},
body: JSON.stringify({
province: province,
@@ -455,79 +437,6 @@
.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));
}
/**
* 地理编码并在地图标记终点
*/
@@ -538,6 +447,7 @@
if (destMarker) destMarker.setMap(null);
// 2. 创建自定义图标
// 假设图标大小为 32x32你可以根据实际图片尺寸调整 Size
var destIcon = new AMap.Icon({
size: new AMap.Size(32, 32), // 图标尺寸
image: 'ic_tag.png', // 本地图片路径
@@ -549,6 +459,8 @@
map: map,
position: [longitude, latitude],
icon: destIcon, // 使用自定义图标
// 偏移量如果图标底部中心是尖角offset 设为宽的一半的负数,高度的负数
// 这样能确保图片的底部尖端指向地图上的精确位置
offset: new AMap.Pixel(-16, -32),
title: name,
label: {
@@ -557,7 +469,17 @@
}
});
console.log("JS->: 终点标记已添加", address);
// 4. 打印调试信息
console.log("JS->: 终点标记已添加", address, loc.toString());
// 5. 自动调整视野包含起点和终点
// if (marker) {
// // 如果起点标志已存在,缩放地图以展示两者
// map.setFitView([marker, destMarker], false, [60, 60, 60, 60]);
// } else {
// // 如果没有起点,直接跳到终点
// map.setCenter(loc);
// }
}
/**
@@ -576,12 +498,11 @@
}
}
/**
* 路径规划
* @param {AMap.LngLat} [destLoc] 可选的终点坐标
* 路径规划
*/
function startRouteSearch(destLoc) {
function startRouteSearch() {
// 获取输入框的文字
var startKw = document.getElementById('startInput').value;
var endKw = document.getElementById('endInput').value;
@@ -589,59 +510,63 @@
alert("请输入起点");
return;
}
if (!endKw) {
alert("请输入终点");
return;
}
// 清除旧路线
if (driving) driving.clear();
// 收起键盘
document.getElementById('startInput').blur();
document.getElementById('endInput').blur();
// --- 构造路径规划的点 (使用数组方式,更灵活) ---
var points = [];
// 1. 起点逻辑
if (!startKw || startKw === '我的位置' || startKw.includes('当前位置')) {
// 1. 处理起点逻辑
// 如果输入框是空的,或者写着 "我的位置",则使用 GPS 坐标
if (!startKw || startKw === '我的位置') {
if (!currentLng || !currentLat) {
if (window.flutter_inappwebview) window.flutter_inappwebview.callHandler('requestLocation');
// 如果还没获取到定位
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler('requestLocation');
}
alert("正在获取定位,请稍后...");
return;
}
// 使用精准坐标对象 (避免高德去猜 '我的位置' 关键词)
points.push({
keyword: '我的位置',
location: new AMap.LngLat(currentLng, currentLat)
keyword: '我的位置', // 用于显示的名字
location: new AMap.LngLat(currentLng, currentLat) // 实际导航用的坐标
});
} else {
// 如果用户手动输入了地点 (例如 "北京南站")
// 直接存入关键词,让高德自己去搜
points.push({
keyword: startKw
});
}
// 2. 终点逻辑:如果有传入坐标,则直接使用坐标导航,成功率最高
if (destLoc) {
points.push({
keyword: endKw,
location: destLoc // 关键:使用精确坐标
});
} else {
points.push({
keyword: endKw
});
}
// 2. 处理终点逻辑 (通常是关键词)
points.push({
keyword: endKw
});
// 3. 发起搜索
// points 数组里现在是一个起点对象和一个终点对象
driving.search(points, function (status, result) {
if (status === 'complete') {
console.log('JS: 规划成功');
var panel = document.getElementById('panel');
panel.style.display = 'block';
document.body.classList.add('panel-active');
} else {
console.log('JS: 规划失败', result);
alert("规划失败,请检查起终点名称");
}
// else {
// console.error('JS: 规划失败', result);
// // 如果坐标规划都失败了,通常是由于起终点距离过近或政策限制(如货车禁行)
// alert("路径规划未成功,请尝试微调起终点");
// }
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 B

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -0,0 +1,50 @@
#
# Be sure to run `pod lib lint AMapNavIOSSDK.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'AMapNavIOSSDK'
s.version = '0.1.0'
s.summary = 'A short description of AMapNavIOSSDK.'
# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
# * Try to keep it short, snappy and to the point.
# * Write the description between the DESC delimiters below.
# * Finally, don't worry about the indent, CocoaPods strips it!
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://github.com/xiaoshuai/AMapNavIOSSDK'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'xiaoshuai' => 'xiaoshuai@net.cn' }
s.source = { :git => 'https://github.com/xiaoshuai/AMapNavIOSSDK.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '12.0'
s.source_files = 'AMapNavIOSSDK/Classes/**/*'
s.resource = 'AMapNavIOSSDK/**/*.bundle'
s.resource_bundles = {
'AMapNavIOSSDKPrivacyInfo' => ['AMapNavIOSSDK/**/PrivacyInfo.xcprivacy']
}
# s.public_header_files = 'Pod/Classes/**/*.h'
# s.frameworks = 'UIKit', 'MapKit'
# s.dependency 'AFNetworking', '~> 2.3'
s.dependency 'Masonry'
s.dependency 'MJExtension'
s.dependency 'AMapNavi-NO-IDFA'
s.dependency 'AMapLocation-NO-IDFA'
s.dependency 'AMapSearch-NO-IDFA'
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>0A2A.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>85F4.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
</dict>
</plist>

View File

@@ -0,0 +1,24 @@
//
// ABaseViewController.h
// ANavDemo
//
// Created by admin on 2026/2/5.
//
#import <UIKit/UIKit.h>
#import <Masonry/Masonry.h>
#import "AMapNavCommonUtil.h"
#define kRoutePlanBarHeight (self.navigationController.navigationBar.frame.size.height + UIApplication.sharedApplication.statusBarFrame.size.height + 0)
#define kRoutePlanStatusBarHeight (UIApplication.sharedApplication.statusBarFrame.size.height + 0)
NS_ASSUME_NONNULL_BEGIN
@interface ABaseViewController : UIViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,33 @@
//
// ABaseViewController.m
// ANavDemo
//
// Created by admin on 2026/2/5.
//
#import "ABaseViewController.h"
@interface ABaseViewController ()
@end
@implementation ABaseViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end

View File

@@ -0,0 +1,79 @@
//
// AMapHyStationModel.h
// AMapNavIOSSDK
//
// Created by admin on 2026/2/11.
//
#import <Foundation/Foundation.h>
#import <MJExtension/MJExtension.h>
NS_ASSUME_NONNULL_BEGIN
/**
{
"name": "嘉兴经开站",
"shortName": null,
"siteNo": null,
"city": null,
"address": "嘉兴市秀洲区岗山路272号",
"contact": "龚明伟",
"phone": "18888888888",
"type": null,
"coOpMode": null,
"booking": null,
"siteStatus": 0,
"startBusiness": "06:00:00",
"endBusiness": "22:00:00",
"billingMethod": null,
"term": null,
"remark": null,
"longitude": "120.75972800",
"latitude": "30.79962800"
}
*/
@interface AMapHyStationModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy, nullable) NSString *shortName;
@property (nonatomic, copy, nullable) NSString *siteNo;
@property (nonatomic, copy, nullable) NSString *city;
@property (nonatomic, copy, nullable) NSString *address;
@property (nonatomic, copy, nullable) NSString *contact;
@property (nonatomic, copy, nullable) NSString *phone;
@property (nonatomic, copy, nullable) NSString *type;
@property (nonatomic, copy, nullable) NSString *coOpMode;
@property (nonatomic, strong, nullable) NSString * booking;
@property (nonatomic, assign) NSInteger siteStatus;
@property (nonatomic, copy, nullable) NSString *startBusiness;
@property (nonatomic, copy, nullable) NSString *endBusiness;
@property (nonatomic, copy, nullable) NSString *billingMethod;
@property (nonatomic, copy, nullable) NSString *term;
@property (nonatomic, copy, nullable) NSString *remark;
@property (nonatomic, copy, nullable) NSString *longitude;
@property (nonatomic, copy, nullable) NSString *latitude;
@end
/**
{
"code": 0,
"status": true,
"message": "success",
"data": [],
"time": "1770800256408",
"error": null
}
*/
@interface AMapHyResponse : NSObject
@property (nonatomic, assign) NSInteger code;
@property (nonatomic, assign) NSInteger status;
@property (nonatomic, copy, nullable) NSString *message;
@property (nonatomic, copy, nullable) NSString *time;
@property (nonatomic, copy, nullable) NSString *error;
@property(nonatomic , strong)NSArray <AMapHyStationModel * > * data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,21 @@
//
// AMapHyStationModel.m
// AMapNavIOSSDK
//
// Created by admin on 2026/2/11.
//
#import "AMapHyStationModel.h"
@implementation AMapHyStationModel
@end
@implementation AMapHyResponse
+ (NSDictionary *)mj_objectClassInArray {
return @{@"data" : AMapHyStationModel.class};
}
@end

View File

@@ -0,0 +1,12 @@
//
// AMapNavSDKHeader.h
// Pods
//
// Created by admin on 2026/2/10.
//
#ifndef AMapNavSDKHeader_h
#define AMapNavSDKHeader_h
#endif /* AMapNavSDKHeader_h */

View File

@@ -0,0 +1,31 @@
//
// AMapNavSDKManager.h
// Pods
//
// Created by admin on 2026/2/10.
//
#import <Foundation/Foundation.h>
#import "ARoutePlaneController.h"
#define kAMapSDKDebugFlag
NS_ASSUME_NONNULL_BEGIN
@interface AMapNavSDKManager : NSObject
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic , strong) NSString * localCity;
@property (nonatomic, copy) NSString * locationAddressDetail;
@property (nonatomic , strong , readonly) UIViewController * targetVC;
+ (instancetype)sharedManager;
- (void)configWithKey:(NSString*)key;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,72 @@
//
// AMapNavSDKManager.m
// Pods
//
// Created by admin on 2026/2/10.
//
#import "AMapNavSDKManager.h"
#import "AMapPrivacyUtility.h"
#import <AMapFoundationKit/AMapFoundationKit.h>
@interface AMapNavSDKManager ()
@property (nonatomic , strong , readwrite) UIViewController * targetVC;
@end
@implementation AMapNavSDKManager
+ (instancetype)sharedManager {
static AMapNavSDKManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[AMapNavSDKManager alloc] init];
});
return manager;
}
- (instancetype)init {
self = [super init];
if (self) {
_targetVC = [ARoutePlaneController new];
}
return self;
}
- (void)configWithKey:(NSString*)key {
if (1) {
/*
*
*/
// [AMapPrivacyUtility handlePrivacyAgreeStatusIn:_targetVC];
//
// SDK
[self configureAPIKey:key];
}
}
- (UIViewController *)targetVC {
return _targetVC;
}
#pragma mark - private
- (void)configureAPIKey:(NSString*)key {
if ([key length] == 0)
{
NSString *reason = [NSString stringWithFormat:@"apiKey为空请检查key是否正确设置。"];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"提示" message:reason delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
[alert show];
}
[AMapServices sharedServices].enableHTTPS = YES;
[AMapServices sharedServices].apiKey = (NSString *)key;
}
@end

View File

@@ -0,0 +1,22 @@
//
// ARoutePlaneController.h
// ANavDemo
//
// Created by admin on 2026/2/5.
//
#import <UIKit/UIKit.h>
#import "ABaseViewController.h"
#import "SelectableOverlay.h"
#import "NaviPointAnnotation.h"
#import <AMapNaviKit/AMapNaviKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ARoutePlaneController : ABaseViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,930 @@
//
// ARoutePlaneController.m
// ANavDemo
//
// Created by admin on 2026/2/5.
//
#import "ARoutePlaneController.h"
#import <AMapNaviKit/MAMapKit.h>
#import <AMapNaviKit/AMapNaviKit.h>
#import <AMapLocationKit/AMapLocationKit.h>
#import <AMapSearchKit/AMapSearchAPI.h>
#import "ASearchAddressController.h"
#import "AMapNavSDKManager.h"
#import "AMapPrivacyUtility.h"
#define kRouteIndicatorViewHeight 64.f
#import "AMapHyStationModel.h"
#import "AMapNavHttpUtil.h"
@interface ARoutePlaneController ()<MAMapViewDelegate, AMapNaviDriveManagerDelegate,AMapNaviCompositeManagerDelegate , AMapLocationManagerDelegate , UITextFieldDelegate >
@property (nonatomic, strong) UITextField *textField;
@property (nonatomic, strong) MAMapView *mapView;
@property (nonatomic,strong) AMapLocationManager *locationService; //
/**
*
*/
@property (nonatomic, assign) double latitude;
@property (nonatomic, assign) double longitude;
@property (nonatomic, strong) UITextField *startTf;
@property (nonatomic, strong) UITextField *dstTf;
@property (nonatomic, strong) UIButton *navBtn;
@property (nonatomic, strong) AMapPOI *startPoi;
@property (nonatomic, strong) AMapPOI *dstPoi;
@property (nonatomic, strong) AMapNaviCompositeManager *compositeManager;//nav
@property (nonatomic, assign) BOOL calRouteSuccess;
@property (nonatomic, strong) NSDictionary * currentCalRoutePaths;//线
@property (nonatomic , assign)BOOL isStartNav;//
@property (nonatomic, strong) NSArray * lastOverLays;
@property (nonatomic , strong)NSArray * hyStationArr;//
@property (nonatomic , assign)BOOL startQueryCurrnetNodeFlag;//
@end
@implementation ARoutePlaneController
- (void)viewDidLoad {
[super viewDidLoad];
_startQueryCurrnetNodeFlag = NO;
[self observePrivacyStatus];
[self checkPrivacyStatus];
////
// [self.naviManager independentCalculateDriveRouteWithStartPOIInfo:startPOIInfo
// endPOIInfo:endPOIInfo
// wayPOIInfos:wayPOIInfos
// strategy:AMapNaviDrivingStrategyMultipleDefault
// callback:^(AMapNaviRouteGroup *routeGroup, NSError *error) {
// if (error == nil) {
// // routeGroup 线
// [self startNaviWithRoute:routeGroup];
// }
// }];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[AMapPrivacyUtility handlePrivacyAgreeStatusIn:self];
}
#pragma mark - request
-(void)requestHyListWithParms:(NSDictionary*)dic {
NSString * url = @"https://beta-esg.api.lnh2e.com/appointment/station/getNearbyHydrogenStationsByLocation";
/**
//
"longitude": "121.30461400",
"latitude": "31.17321100"
*/
// NSDictionary * dic = @{@"longitude":@"121.16661700" , @"latitude":@"31.27981600"};
[AMapNavHttpUtil postRequestWithURL:url parameters:dic requestHeader:@{@"Content-Type":@"application/json; charset=UTF-8"} successHandler:^(NSDictionary * _Nonnull data, NSURLResponse * _Nonnull response) {
AMapHyResponse * resp = [AMapHyResponse mj_objectWithKeyValues:data];
if (resp.code == 0 && resp.data) {
NSArray * allData = resp.data;
NSArray * dst = allData;
NSInteger len = allData.count;
if (allData.count > len) {
dst = [resp.data subarrayWithRange:NSMakeRange(0, len)];
}
[self updateMapAnnotationWithData:dst];
}else {
NSLog(@">>>>>>>请求站点:%@" ,resp.message);
}
} failureHandler:^(NSError * _Nonnull error) {
NSLog(@">>>>>>>请求站点err%@" ,error.debugDescription);
}];
}
-(void)requestHyDetailWithParms:(NSDictionary*)dic {
NSString * url = @"https://beta-esg.api.lnh2e.com/appointment/station/getStationInfoByArea";
[AMapNavHttpUtil postRequestWithURL:url parameters:dic requestHeader:@{@"Content-Type":@"application/json; charset=UTF-8"} successHandler:^(NSDictionary * _Nonnull data, NSURLResponse * _Nonnull response) {
AMapHyResponse * resp = [AMapHyResponse mj_objectWithKeyValues:data];
if (resp.code == 0) {
NSDictionary * resData = data[@"data"];
AMapHyStationModel * station = [AMapHyStationModel mj_objectWithKeyValues:resData];
[self updateHeadAddressWithStation:station];
}else {
NSLog(@">>>>>>>请求站点detail%@" ,resp.message);
}
} failureHandler:^(NSError * _Nonnull error) {
NSLog(@">>>>>>>请求站点err%@" ,error.debugDescription);
}];
}
-(void)updateHeadAddressWithStation:(AMapHyStationModel*)model {
AMapPOI * aoi = [[AMapPOI alloc] init];
aoi.location = [AMapGeoPoint locationWithLatitude:[model.latitude doubleValue] longitude:[model.longitude doubleValue]];
aoi.name = model.name;
self.dstPoi = aoi;
///
[self updateUIWithData:aoi textField:self.dstTf];
///
[self updateMapAnnotationWithData:@[model]];
}
-(void)updateMapAnnotationWithData:(NSArray *)dataArr {
self.hyStationArr = dataArr;
if (!(dataArr && dataArr.count > 0)) {
return;
}
///
NSMutableArray * points = [NSMutableArray arrayWithCapacity:dataArr.count];
for (AMapHyStationModel * model in dataArr) {
MAPointAnnotation *pointAnnotation = [[MAPointAnnotation alloc] init];
if (!(model.latitude && model.longitude)) {
continue;
}
pointAnnotation.coordinate = CLLocationCoordinate2DMake([model.latitude doubleValue], [model.longitude doubleValue]);
pointAnnotation.title = model.name;
[points addObject:pointAnnotation];
}
// 1.
// MACoordinateRegion currentRegion = self.mapView.region;
// 2.
[self.mapView addAnnotations:points];
//
if (self.latitude && self.longitude) {
[self.mapView setCenterCoordinate: CLLocationCoordinate2DMake(self.latitude, self.longitude) animated:YES];
}
// 3.
// [self.mapView setRegion:currentRegion animated:NO];
}
#pragma mark -
-(void)initSubview {
UITextField * startTf = [[UITextField alloc] init];
startTf.borderStyle = UITextBorderStyleRoundedRect;
startTf.placeholder = @"起点";
startTf.tag = 100;
startTf.delegate = self;
startTf.font = [UIFont systemFontOfSize:13];
[self.view addSubview:startTf];
[startTf mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.view).offset(kRoutePlanStatusBarHeight + 35);
make.left.mas_equalTo(self.view).offset(5);
make.width.mas_equalTo(@120);
make.height.mas_equalTo(@32);
}];
self.startTf = startTf;
UITextField * dstTf = [[UITextField alloc] init];
dstTf.borderStyle = UITextBorderStyleRoundedRect;
dstTf.placeholder = @"终点";
dstTf.tag = 200;
dstTf.delegate = self;
dstTf.font = [UIFont systemFontOfSize:13];
[self.view addSubview:dstTf];
[dstTf mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.mas_equalTo(startTf);
make.left.mas_equalTo(startTf.mas_right).offset(15);
// make.right.mas_equalTo(self.view).offset(-5);
make.width.height.mas_equalTo(startTf);
}];
self.dstTf = dstTf;
UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setTitle:@"规划线路" forState:UIControlStateNormal];
btn.backgroundColor = [UIColor whiteColor];
btn.titleLabel.font = [UIFont systemFontOfSize:14];
btn.layer.borderColor = [UIColor blueColor].CGColor;
btn.layer.borderWidth = 1;
btn.layer.cornerRadius = 5;
[btn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[btn addTarget:self action:@selector(calRoutePath) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(dstTf.mas_right).offset(12);
make.top.mas_equalTo(startTf);
make.right.mas_equalTo(self.view).offset(-5);
make.height.mas_equalTo(@30);
}];
UIButton * navBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[navBtn setTitle:@"导航>" forState:UIControlStateNormal];
navBtn.backgroundColor = [UIColor whiteColor];
navBtn.titleLabel.font = [UIFont systemFontOfSize:14];
navBtn.layer.borderColor = [UIColor blueColor].CGColor;
navBtn.layer.borderWidth = 1;
navBtn.layer.cornerRadius = 6;
[navBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[navBtn addTarget:self action:@selector(navAction) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:navBtn];
[navBtn mas_makeConstraints:^(MASConstraintMaker *make) {
// make.left.equalTo(self.view).offset(12);
// make.centerX.equalTo(self.view);
make.right.equalTo(self.view).offset(-15);
make.bottom.equalTo(self.view).offset(-118);
make.height.mas_equalTo(@30);
make.width.mas_equalTo(@60);
}];
[self.mapView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view);
// make.top.equalTo(startTf.mas_bottom).offset(5);
make.top.equalTo(self.view).offset(0);
// make.bottom.equalTo(navBtn.mas_top).offset(-3);
make.bottom.equalTo(self.view).offset(0);
}];
[self.view bringSubviewToFront:navBtn];
}
- (void)initDriveManager
{
// dealloc [AMapNaviDriveManager destroyInstance]
[[AMapNaviDriveManager sharedInstance] setDelegate:self];
}
- (void)initMapView
{
if (self.mapView == nil)
{
self.mapView = [[MAMapView alloc] initWithFrame:CGRectZero];
[self.mapView setDelegate:self];
self.mapView.showsUserLocation = YES;
self.mapView.userTrackingMode = MAUserTrackingModeFollowWithHeading;
self.mapView.desiredAccuracy = kCLLocationAccuracyNearestTenMeters; //
_mapView.showsScale= YES;
CGFloat ze = self.mapView.zoomLevel;
self.mapView.zoomLevel = 9;
// 2.
// self.mapView.autoresizesSubviews = NO;
// [self.mapView setShowsWorldMap:NO]; //
// 3.
// [self.mapView setMinZoomLevel:6.0];
// [self.mapView setMaxZoomLevel:20.0];
// [self.mapView setZoomLevel:10.0 animated:NO];
// 4.
// [self.mapView setAutoCheckMapBoundary:NO];
[self.view addSubview:self.mapView];
if (@available(iOS 14.0, *)) {
// iOS14+
CLAuthorizationStatus status = [[[CLLocationManager alloc] init] authorizationStatus];
if (status == kCLAuthorizationStatusNotDetermined) {
[[[CLLocationManager alloc] init] requestWhenInUseAuthorization];
}
}
///TEST
// MAPointAnnotation *pointAnnotation = [[MAPointAnnotation alloc] init];
// pointAnnotation.coordinate = CLLocationCoordinate2DMake(31.19, 121.32);
// pointAnnotation.title = @"嘉兴经开站";
// [_mapView addAnnotation:pointAnnotation];
//
// MAPointAnnotation *pointAnnotation2 = [[MAPointAnnotation alloc] init];
// pointAnnotation2.coordinate = CLLocationCoordinate2DMake(30.81669400, 120.94291800);
// pointAnnotation2.title = @"测试站点1";
// [_mapView addAnnotation:pointAnnotation2];
UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setImage:[AMapNavCommonUtil imageWithName:@"icon_local"] forState:UIControlStateNormal];
btn.backgroundColor = [UIColor lightGrayColor];
btn.titleLabel.font = [UIFont systemFontOfSize:14];
btn.layer.cornerRadius = 20;
[btn addTarget:self action:@selector(updateUserLocalAction) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.view).offset(-10);
make.width.height.equalTo(@40);
make.top.equalTo(self.view).offset(150);
}];
}
}
-(void)updateUserLocalAction {
//
if (_mapView.userLocation.location) {
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(self.latitude, self.longitude);
[_mapView setCenterCoordinate:coord animated:YES];
// [_mapView setZoomLevel:10 animated:YES];
} else {
//
[_mapView setUserTrackingMode:MAUserTrackingModeFollow animated:YES];
}
}
- (void)initAnnotations
{
NaviPointAnnotation *beginAnnotation = [[NaviPointAnnotation alloc] init];
[beginAnnotation setCoordinate:CLLocationCoordinate2DMake(self.startPoi.location.latitude, self.startPoi.location.longitude)];
beginAnnotation.title = @"起始点";
beginAnnotation.navPointType = NaviPointAnnotationStart;
[self.mapView addAnnotation:beginAnnotation];
// NaviPointAnnotation *endAnnotation = [[NaviPointAnnotation alloc] init];
// [endAnnotation setCoordinate:CLLocationCoordinate2DMake(self.dstPoi.location.latitude, self.dstPoi.location.longitude)];
// endAnnotation.title = @"终点";
// endAnnotation.navPointType = NaviPointAnnotationEnd;
//
// [self.mapView addAnnotation:endAnnotation];
}
- (AMapLocationManager *)locationService {
if (!_locationService) {
_locationService = [[AMapLocationManager alloc] init];
_locationService.delegate = self;
_locationService.desiredAccuracy = kCLLocationAccuracyBest; //
_locationService.distanceFilter = 5;
_locationService.locatingWithReGeocode = YES;
}
return _locationService;
}
- (void)dealloc {
[self.locationService stopUpdatingLocation];
}
- (AMapNaviCompositeManager *)compositeManager {
if (!_compositeManager) {
_compositeManager = [[AMapNaviCompositeManager alloc] init]; //
_compositeManager.delegate = self; // 使AMapNaviCompositeManagerDelegatedelegate
}
return _compositeManager;
}
//
- (void)observePrivacyStatus {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handlePrivacyUpdate)
name:@"ksAMapPrivacyDidUpdateNotification" //
object:nil];
}
-(void)handlePrivacyUpdate {
[self checkPrivacyStatus];
}
//
- (void)checkPrivacyStatus {
BOOL hasAgreed = [[NSUserDefaults standardUserDefaults] boolForKey:@"usragreeStatus"];
if (hasAgreed) {
///
[self.locationService startUpdatingLocation];
// [self.mapView reloadMap];
[self initMapView];
[self initSubview];
[self initDriveManager];
} else {
}
}
#pragma mark - Action
///
-(void)navAction {
[self showSelectNavType];
}
-(void)showSelectNavType {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"选择导航类型" message:nil preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction *sure = [UIAlertAction actionWithTitle:@"SDK导航" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[self navigationType_sdk];
}];
UIAlertAction *sure2 = [UIAlertAction actionWithTitle:@"高德地图导航" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[self navigationType_app];
}];
UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
}];
[alert addAction:sure];
[alert addAction:sure2];
[alert addAction:cancel];
[self presentViewController:alert animated:YES completion:nil];
}
-(void)navigationType_app {
NSURL* scheme = [NSURL URLWithString:@"iosamap://"];
BOOL canOpen = [[UIApplication sharedApplication] canOpenURL:scheme];
if (!canOpen) {
[self showAlertWithMessage:@"请先安装高德地图客户端"]; return;
}
NSString *myLocationScheme = [NSString stringWithFormat:@"iosamap://navi?sourceApplication=ANavDemo&lat=31.2304&lon=121.4737&t=0&dev=1"];
NSString *encodedUrlString = [myLocationScheme stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSURL *gaodeUrl = [NSURL URLWithString:encodedUrlString];
[[UIApplication sharedApplication] openURL:gaodeUrl options:@{} completionHandler:^(BOOL res) {
}];
}
-(void)navigationType_sdk {
id delegate = [AMapNaviDriveManager sharedInstance].delegate;
if (!delegate) {
[AMapNaviDriveManager sharedInstance].delegate = self;
}
NSDictionary * routes = [AMapNaviDriveManager sharedInstance].naviRoutes;
if (!routes) {
NSLog(@"暂无路线信息!!!!!!!!!");
self.isStartNav = YES;
[self calRoutePath];
return;
}
AMapNaviCompositeUserConfig *config = [[AMapNaviCompositeUserConfig alloc] init];
// [config setRoutePlanPOIType:AMapNaviRoutePlanPOITypeEnd location:[AMapNaviPoint locationWithLatitude:32.21 longitude:121.34] name:@"故宫22" POIId:nil];
[config setStartNaviDirectly:YES]; //
[config setNeedCalculateRouteWhenPresent:NO];//
[config setMultipleRouteNaviMode:NO];//线
// [config setNeedDestoryDriveManagerInstanceWhenDismiss:NO];
self.isStartNav = NO;
[self.compositeManager presentRoutePlanViewControllerWithOptions:config];
}
#pragma mark - 线
///线
-(void)calRoutePath {
// [self.mapView removeOverlays:self.mapView.overlays];
// self.startTf.text = @"click calpath";
// return;
[self initAnnotations];
AMapNaviPoint * startPoint = [AMapNaviPoint locationWithLatitude:self.startPoi.location.latitude longitude:self.startPoi.location.longitude];
AMapNaviPoint * endPoint = [AMapNaviPoint locationWithLatitude:self.dstPoi.location.latitude longitude:self.dstPoi.location.longitude];
AMapNaviDrivingStrategy strategy = ConvertDrivingPreferenceToDrivingStrategy(0,
0,
0,
0,
0);
id delegate = [AMapNaviDriveManager sharedInstance].delegate;
if (!delegate) {
[AMapNaviDriveManager sharedInstance].delegate = self;
}
[[AMapNaviDriveManager sharedInstance] calculateDriveRouteWithStartPoints:@[startPoint]
endPoints:@[endPoint]
wayPoints:nil
drivingStrategy:strategy];
}
- (void)driveManagerOnCalculateRouteSuccess:(AMapNaviDriveManager *)driveManager
{
NSLog(@"onCalculateRouteSuccess");
//
[self showNaviRoutes];
}
- (void)showNaviRoutes
{
if ([[AMapNaviDriveManager sharedInstance].naviRoutes count] <= 0)
{
return;
}
self.lastOverLays = self.mapView.overlays;
[self.mapView removeOverlays:self.mapView.overlays];
// [self.routeIndicatorInfoArray removeAllObjects];
self.currentCalRoutePaths = [AMapNaviDriveManager sharedInstance].naviRoutes;
NSInteger routeId = 0;
//
for (NSNumber *aRouteID in [[AMapNaviDriveManager sharedInstance].naviRoutes allKeys])
{
AMapNaviRoute *aRoute = [[[AMapNaviDriveManager sharedInstance] naviRoutes] objectForKey:aRouteID];
int count = (int)[[aRoute routeCoordinates] count];
//Polyline
CLLocationCoordinate2D *coords = (CLLocationCoordinate2D *)malloc(count * sizeof(CLLocationCoordinate2D));
for (int i = 0; i < count; i++)
{
AMapNaviPoint *coordinate = [[aRoute routeCoordinates] objectAtIndex:i];
coords[i].latitude = [coordinate latitude];
coords[i].longitude = [coordinate longitude];
}
MAPolyline *polyline = [MAPolyline polylineWithCoordinates:coords count:count];
SelectableOverlay *selectablePolyline = [[SelectableOverlay alloc] initWithOverlay:polyline];
[selectablePolyline setRouteID:[aRouteID integerValue]];
[self.mapView addOverlay:selectablePolyline];
free(coords);
routeId = [aRouteID integerValue];
}
// 1.
MACoordinateRegion currentRegion = self.mapView.region;
[self.mapView showAnnotations:self.mapView.annotations animated:NO];
// 3.
[self.mapView setRegion:currentRegion animated:NO];
[self selectNaviRouteWithID:routeId];
///
if (self.isStartNav) {
[self navigationType_sdk];
}
}
- (void)selectNaviRouteWithID:(NSInteger)routeID
{
//
if ([[AMapNaviDriveManager sharedInstance] selectNaviRouteWithRouteID:routeID])
{
[self selecteOverlayWithRouteID:routeID];
}
else
{
NSLog(@"路径选择失败!");
}
}
- (void)selecteOverlayWithRouteID:(NSInteger)routeID
{
[self.mapView.overlays enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id<MAOverlay> overlay, NSUInteger idx, BOOL *stop)
{
if ([overlay isKindOfClass:[SelectableOverlay class]])
{
SelectableOverlay *selectableOverlay = overlay;
/* overlayrenderer. */
MAPolylineRenderer * overlayRenderer = (MAPolylineRenderer *)[self.mapView rendererForOverlay:selectableOverlay];
if (selectableOverlay.routeID == routeID)
{
/* . */
selectableOverlay.selected = YES;
/* renderer. */
overlayRenderer.fillColor = selectableOverlay.selectedColor;
overlayRenderer.strokeColor = selectableOverlay.selectedColor;
/* overlay. */
[self.mapView exchangeOverlayAtIndex:idx withOverlayAtIndex:self.mapView.overlays.count - 1];
}
else
{
/* . */
selectableOverlay.selected = NO;
/* renderer. */
overlayRenderer.fillColor = selectableOverlay.regularColor;
overlayRenderer.strokeColor = selectableOverlay.regularColor;
}
}
}];
}
#pragma mark - AMapLocationManagerDelegate
- (void)amapLocationManager:(AMapLocationManager *)manager didUpdateLocation:(CLLocation *)location reGeocode:(AMapLocationReGeocode *)reGeocode
{
if (!location) {
return;
}
self.latitude = location.coordinate.latitude;
self.longitude = location.coordinate.longitude;
AMapNavSDKManager * sdk = [AMapNavSDKManager sharedManager];
sdk.localCity = reGeocode.city;
sdk.locationAddressDetail = reGeocode.POIName;
//
// MACoordinateRegion region = MACoordinateRegionMake(location.coordinate,
// MACoordinateSpanMake(0.1, 0.1));
// [self.mapView setRegion:region animated:YES];
//
AMapPOI * aoi = [[AMapPOI alloc] init];
#ifdef kAMapSDKDebugFlag
aoi.location = [AMapGeoPoint locationWithLatitude:31.23 longitude:121.48 ];
aoi.name =@"人民大道185号";
#else
aoi.location = [AMapGeoPoint locationWithLatitude:self.latitude longitude:self.longitude ];
aoi.name = reGeocode.POIName;
#endif
self.startPoi = aoi;
[self updateUIWithData:aoi textField:self.startTf];
//
if (!self.startQueryCurrnetNodeFlag && reGeocode) {
self.startQueryCurrnetNodeFlag = YES;
NSString * province = reGeocode.province;
NSString * city = reGeocode.city;
NSString * district = reGeocode.district;
NSString * longitude = [NSString stringWithFormat:@"%f",self.longitude];
NSString * latitude = [NSString stringWithFormat:@"%f",self.latitude];
if (province && city && district) {
NSDictionary * dic = @{@"province":province , @"city":city , @"district":district , @"longitude":longitude , @"latitude":latitude};
[self requestHyDetailWithParms:dic];
}
[self requestHyListWithParms:@{@"longitude":longitude , @"latitude":latitude}];
}
}
#pragma mark - MAMapView
- (MAAnnotationView *)mapView:(MAMapView *)mapView viewForAnnotation:(id<MAAnnotation>)annotation
{
if ([annotation isKindOfClass:[NaviPointAnnotation class]])
{
static NSString *annotationIdentifier = @"NaviPointAnnotationIdentifier";
MAPinAnnotationView *pointAnnotationView = (MAPinAnnotationView*)[self.mapView dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];
if (pointAnnotationView == nil)
{
pointAnnotationView = [[MAPinAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:annotationIdentifier];
}
pointAnnotationView.animatesDrop = NO;
pointAnnotationView.canShowCallout = YES;
pointAnnotationView.draggable = NO;
NaviPointAnnotation *navAnnotation = (NaviPointAnnotation *)annotation;
if (navAnnotation.navPointType == NaviPointAnnotationStart)
{
[pointAnnotationView setPinColor:MAPinAnnotationColorGreen];
}
else if (navAnnotation.navPointType == NaviPointAnnotationEnd)
{
[pointAnnotationView setPinColor:MAPinAnnotationColorRed];
}
return pointAnnotationView;
}
if ( [annotation isMemberOfClass:[MAPointAnnotation class]])
{
MAUserLocation *user = (MAUserLocation *)annotation;
static NSString *pointReuseIndentifier = @"pointReuseIndentifier";
MAPinAnnotationView*annotationView = (MAPinAnnotationView*)[mapView dequeueReusableAnnotationViewWithIdentifier:pointReuseIndentifier];
if (annotationView == nil)
{
annotationView = [[MAPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:pointReuseIndentifier];
}
annotationView.canShowCallout= YES; //NO
annotationView.animatesDrop = NO; //NO
annotationView.draggable = NO; //NO
annotationView.pinColor = MAPinAnnotationColorPurple;
//
if (@available(iOS 14.0, *)) {
// iOS 14+ 使 tintColor
annotationView.tintColor = [UIColor systemBlueColor];
} else {
// iOS 13
annotationView.tintColor = [UIColor colorWithRed:0.1 green:0.6 blue:0.9 alpha:1.0];
}
return annotationView;
}
return nil;
}
- (MAOverlayRenderer *)mapView:(MAMapView *)mapView rendererForOverlay:(id<MAOverlay>)overlay
{
if ([overlay isKindOfClass:[SelectableOverlay class]])
{
SelectableOverlay * selectableOverlay = (SelectableOverlay *)overlay;
id<MAOverlay> actualOverlay = selectableOverlay.overlay;
MAPolylineRenderer *polylineRenderer = [[MAPolylineRenderer alloc] initWithPolyline:actualOverlay];
polylineRenderer.lineWidth = 8.f;
polylineRenderer.strokeColor = selectableOverlay.isSelected ? selectableOverlay.selectedColor : selectableOverlay.regularColor;
return polylineRenderer;
}
return nil;
}
//
- (void)mapView:(MAMapView *)mapView didAddAnnotationViews:(NSArray *)views {
//
# if 0
for (MAAnnotationView *view in views) {
//
if ([view.annotation isMemberOfClass:[MAPointAnnotation class]]) {
// 使
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 使
[mapView selectAnnotation:view.annotation animated:YES];
});
}
}
#endif
}
- (void)mapView:(MAMapView *)mapView didSelectAnnotationView:(MAAnnotationView *)view {
NSLog(@"didSelectAnnotationView: %s" , __func__);
}
//
- (void)mapView:(MAMapView *)mapView didDeselectAnnotationView:(MAAnnotationView *)view {
if ([view.annotation isMemberOfClass:[MAPointAnnotation class]]) {
//
// [mapView selectAnnotation:view.annotation animated:NO];
}
}
//
- (void)mapView:(MAMapView *)mapView didAnnotationViewCalloutTapped:(MAAnnotationView *)view {
id pointAnnotation = view.annotation;
if ([pointAnnotation isMemberOfClass:MAPointAnnotation.class]) {
MAPointAnnotation *point = (MAPointAnnotation *)view.annotation;
NSLog(@"point: %@" , point.title);
AMapPOI * aoi = [[AMapPOI alloc] init];
aoi.location = [AMapGeoPoint locationWithLatitude:point.coordinate.latitude longitude:point.coordinate.longitude];
aoi.name = point.title;
self.dstPoi = aoi;
[self updateUIWithData:aoi textField:self.dstTf];
}
NSLog(@"didSelectAnnotationView: %s" , __func__);
}
#pragma mark - AMapNaviCompositeManagerDelegate
- (void)compositeManager:(AMapNaviCompositeManager *)compositeManager didStartNavi:(AMapNaviMode)naviMode {
}
- (void)compositeManager:(AMapNaviCompositeManager *)compositeManager onDriveStrategyChanged:(AMapNaviDrivingStrategy)driveStrategy {
NSLog(@"%s" , __func__ );
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
ASearchAddressController * vc = [[ASearchAddressController alloc] init];
UINavigationController * nav = [[UINavigationController alloc]initWithRootViewController:vc];
//UIModalPresentationOverFullScreen/UIModalPresentationFullScreen
nav.modalPresentationStyle = UIModalPresentationOverFullScreen;
nav.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
__weak typeof(self)weakSelf = self;
vc.selectAddressBlk = ^(AMapPOI * _Nonnull poi) {
[weakSelf updateUIWithData:poi textField:textField];
};
[self presentViewController:nav animated:YES completion:^{
}];
return NO;
}
-(void)updateUIWithData: (AMapPOI*)poi textField: (UITextField*)tf {
BOOL isStart = tf.tag == 100;
tf.text = poi.name;
if (isStart) {
self.startPoi = poi;
}else {
self.dstPoi = poi;
}
}
#pragma mark - tool
-(void)showAlertWithMessage:(NSString *)msg {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:msg preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *sure = [UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
}];
[alert addAction:sure];
[self.navigationController presentViewController:alert animated:YES completion:nil];
}
@end

View File

@@ -0,0 +1,22 @@
//
// ASearchAddressController.h
// ANavDemo
//
// Created by admin on 2026/2/6.
//
#import "ABaseViewController.h"
#import <AMapSearchKit/AMapSearchKit.h>
#import <AMapLocationKit/AMapLocationKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ASearchAddressController : ABaseViewController
@property (nonatomic , copy) void(^selectAddressBlk)(AMapPOI * poi);
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,232 @@
//
// ASearchAddressController.m
// ANavDemo
//
// Created by admin on 2026/2/6.
//
#import "ASearchAddressController.h"
#import "AMapNavSDKManager.h"
#import <AMapFoundationKit/AMapFoundationKit.h>
#import "AMapNavCommonUtil.h"
@interface ASearchAddressController ()<UITextFieldDelegate , AMapSearchDelegate,UITableViewDelegate , UITableViewDataSource>
@property (nonatomic , strong) UITableView *tableView;
@property (nonatomic , strong) UIBarButtonItem *rightItem;
@property (nonatomic ,strong)UIButton * backBtn;
@property (nonatomic , strong) NSArray *dataArr;
@property (nonatomic, strong) UITextField *inputAddressTf;
@property (nonatomic, strong) AMapSearchAPI *search;
@end
@implementation ASearchAddressController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"选择地点";
[self initSubview];
self.search = [[AMapSearchAPI alloc] init];
self.search.delegate = self;
#ifdef kAMapSDKDebugFlag
self.inputAddressTf.text = @"人民广场";
#endif
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.inputAddressTf becomeFirstResponder];
}
-(void)initSubview {
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]initWithCustomView:self.backBtn];
UITextField * inputAddressTf = [[UITextField alloc] init];
inputAddressTf.borderStyle = UITextBorderStyleRoundedRect;
inputAddressTf.placeholder = @"输入地址";
inputAddressTf.returnKeyType = UIReturnKeySearch;
inputAddressTf.tag = 100;
inputAddressTf.delegate = self;
[self.view addSubview:inputAddressTf];
[inputAddressTf mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.view).offset(10);
make.top.mas_equalTo(self.view).offset(kRoutePlanBarHeight + 10);
make.height.mas_equalTo(@30);
}];
self.inputAddressTf = inputAddressTf;
UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setTitle:@"当前位置" forState:UIControlStateNormal];
btn.backgroundColor = [UIColor whiteColor];
btn.layer.borderColor = [UIColor blueColor].CGColor;
btn.layer.borderWidth = 1;
btn.layer.cornerRadius = 5;
btn.titleLabel.font = [UIFont systemFontOfSize:12];
[btn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[btn addTarget:self action:@selector(searchBtnAction) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(inputAddressTf.mas_right).offset(12);
make.top.mas_equalTo(inputAddressTf);
make.right.mas_equalTo(self.view).offset(-10);
make.height.mas_equalTo(@30);
make.width.mas_equalTo(70);
}];
[self.view addSubview:self.tableView];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(inputAddressTf.mas_bottom).offset(5);
make.left.equalTo(self.view);
make.bottom.equalTo(self.view);
make.centerX.equalTo(self.view);
}];
[self.tableView reloadData];
}
#pragma mark -
- (nonnull UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
AMapPOI * m = self.dataArr[indexPath.row];
cell.textLabel.text = [NSString stringWithFormat:@"%@" , m.name ];
return cell;
}
- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataArr.count;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
AMapPOI * m = self.dataArr[indexPath.row];
if (self.selectAddressBlk) {
self.selectAddressBlk(m);
}
[self backBtnAction];
// [self.navigationController popViewControllerAnimated:YES];
}
#pragma mark -
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
}
return _tableView;
}
-(UIButton *)backBtn{
if (!_backBtn) {
_backBtn = [UIButton buttonWithType:UIButtonTypeCustom];
_backBtn.frame = CGRectMake(0, 0, 40, 30);
_backBtn.imageEdgeInsets = UIEdgeInsetsMake(2, -5, 2, 5);
// _backBtn.backgroundColor = UIColor.redColor;
[_backBtn setImage:[AMapNavCommonUtil imageWithName:@"icon_fanhui"] forState:UIControlStateNormal];
[_backBtn addTarget:self action:@selector(backBtnAction) forControlEvents:UIControlEventTouchUpInside];
}
return _backBtn;
}
-(void)backBtnAction {
[self dismissViewControllerAnimated:YES completion:^{
}];
}
#pragma mark - Action
-(void)searchBtnAction {
AMapNavSDKManager * sdk = [AMapNavSDKManager sharedManager];
self.inputAddressTf.text = sdk.locationAddressDetail;
[self startSearchWithAddress:self.inputAddressTf.text];
}
-(void)startSearchWithAddress:(NSString *)addr {
if (!addr) {
return;
}
AMapPOIKeywordsSearchRequest *request = [[AMapPOIKeywordsSearchRequest alloc] init];
request.keywords = addr;
AMapNavSDKManager * sdk = [AMapNavSDKManager sharedManager];
request.city = sdk.localCity;
// request.types = @"高等院校";
// request.requireExtension = YES;
request.offset =20;
/* SDK 3.2.0 POI*/
request.cityLimit = YES;
// request.requireSubPOIs = YES;
[self.search AMapPOIKeywordsSearch:request];
}
#pragma mark -
/* POI . */
- (void)onPOISearchDone:(AMapPOISearchBaseRequest *)request response:(AMapPOISearchResponse *)response
{
NSArray * pois = response.pois;
if (pois.count == 0)
{
return;
}
//responsePOI Demo
self.dataArr = [NSArray arrayWithArray:pois];
[self.tableView reloadData];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
[self startSearchWithAddress:textField.text];
return YES;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.inputAddressTf resignFirstResponder];
}
@end

View File

@@ -0,0 +1,19 @@
//
// AMapNavCommonUtil.h
// Pods
//
// Created by admin on 2026/2/11.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AMapNavCommonUtil : NSObject
+(UIImage *)imageWithName:(NSString *)name;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,27 @@
//
// AMapNavCommonUtil.m
// Pods
//
// Created by admin on 2026/2/11.
//
#import "AMapNavCommonUtil.h"
@implementation AMapNavCommonUtil
#pragma mark -
+(UIImage *)imageWithName:(NSString *)name {
NSURL * url = [[NSBundle mainBundle] URLForResource:@"AMapNavIOSSDK" withExtension:@"bundle"];
NSBundle *containnerBundle = [NSBundle bundleWithURL:url];
NSString * path = [containnerBundle pathForResource:[NSString stringWithFormat:@"%@@2x.png" , name] ofType:nil];
UIImage * arrowImage = [[UIImage imageWithContentsOfFile:path] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
return arrowImage;
}
@end

View File

@@ -0,0 +1,19 @@
//
// AMapNavHttpUtil.h
// AMapNavIOSSDK
//
// Created by admin on 2026/2/11.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AMapNavHttpUtil : NSObject
+ (void)postRequestWithURL:(NSString *)urlString parameters:(id)parameters requestHeader:(NSDictionary *)headParam successHandler:(void (^)(NSDictionary *data, NSURLResponse *response))successHandler failureHandler:(void ( ^)(NSError *error))failureHandler;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,114 @@
//
// AMapNavHttpUtil.m
// AMapNavIOSSDK
//
// Created by admin on 2026/2/11.
//
#import "AMapNavHttpUtil.h"
#define AMapRequestMethod_POST @"POST"
@interface AMapNavHttpUtil ()
@property (nonatomic , copy) NSString * baseURL;
@end
@implementation AMapNavHttpUtil
+ (instancetype)sharedInstance {
static AMapNavHttpUtil *sharedInstance = nil;
static dispatch_once_t onceToken;
// dispatch_once
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
//
});
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
// if (sdk.config.developmentModel) {
// _baseURL = kYTOTPAnalyticsSDKTestHost;
// }else {
// _baseURL = kYTOTPAnalyticsSDKProductionHost;
// }
}
return self;
}
+ (void)postRequestWithURL:(NSString *)urlString parameters:(id)parameters requestHeader:(NSDictionary *)headParam successHandler:(void (^)(NSDictionary *data, NSURLResponse *response))successHandler failureHandler:(void ( ^)(NSError *error))failureHandler {
[self requestWithMethod:AMapRequestMethod_POST URL:urlString parameters:parameters requestHeader:headParam successHandler:successHandler failureHandler:failureHandler];
}
// 使NSURLSession
+ (void)requestWithMethod:(NSString *)method URL:(NSString *)urlString parameters:(id)parameters requestHeader:(NSDictionary *)headParam successHandler:(void (^)(NSDictionary *data, NSURLResponse *response))successHandler failureHandler:(void (^)(NSError *error))failureHandler {
if (!urlString) {
return;
}
// URL
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@" , urlString]];
//
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = method;
request.timeoutInterval = 30.0;
// json
if (headParam) {
for (NSString *key in headParam.allKeys) {
if (headParam[key]) {
[request setValue:[NSString stringWithFormat:@"%@",headParam[key]] forHTTPHeaderField:key];
}
}
}
// JSON
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:&error];
if (!jsonData) {}
if ([method isEqualToString: AMapRequestMethod_POST]) {
//
request.HTTPBody = jsonData;
}
__block NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"request error:%@" , error);
if (failureHandler) {
failureHandler(error);
}
} else {
if (successHandler) {
NSDictionary * dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
if(dic){
NSLog(@"url: %@ , response data:%@", url , dic);
}
dispatch_async(dispatch_get_main_queue(), ^{
successHandler(dic, response);
});
// successHandler(dic, response);
}
}
[session finishTasksAndInvalidate];
session = nil;
}];
//
[task resume];
}
@end

View File

@@ -0,0 +1,32 @@
//
// AMapPrivacyUtility.h
// officialDemoNavi
//
// Created by menglong on 2021/10/29.
// Copyright © 2021 AutoNavi. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/*
* 隐私合规使用demo 工具类
*/
@interface AMapPrivacyUtility : NSObject
/**
* @brief 通过这个方法来判断是否同意隐私合规
* 1.如果没有同意隐私合规则创建的SDK manager 实例返回 为nil 无法使用SDK提供的功能
* 2.如果同意了下次启动不提示 的授权,则不会弹框给用户
* 3.如果只同意了,则下次启动还要给用户弹框提示
*/
+ (void)handlePrivacyAgreeStatus;
+ (void)handlePrivacyAgreeStatusIn:(UIViewController*)targetVC;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,137 @@
//
// AMapPrivacyUtility.m
// officialDemoNavi
//
// Created by menglong on 2021/10/29.
// Copyright © 2021 AutoNavi. All rights reserved.
//
#import "AMapPrivacyUtility.h"
#import <UIKit/UIKit.h>
#import <AMapNavikit/AMapNaviManagerConfig.h>
@implementation AMapPrivacyUtility
+ (void)showPrivacyInfoInWindow:(UIWindow *)window {
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = NSTextAlignmentLeft;
NSMutableAttributedString *privacyInfo = [[NSMutableAttributedString alloc] initWithString:@"\n亲感谢您对XXX一直以来的信任我们依据最新的监管要求更新了XXX《隐私权政策》特向您说明如下\n1.为向您提供交易相关基本功能,我们会收集、使用必要的信息;\n2.基于您的明示授权,我们可能会获取您的位置(为您提供附近的商品、店铺及优惠资讯等)等信息,您有权拒绝或取消授权;\n3.我们会采取业界先进的安全措施保护您的信息安全;\n4.未经您同意,我们不会从第三方处获取、共享或向提供您的信息;" attributes:@{
NSParagraphStyleAttributeName:paragraphStyle,
}];
[privacyInfo addAttribute:NSLinkAttributeName
value:@"《隐私权政策》"
range:[[privacyInfo string] rangeOfString:@"《隐私权政策》"]];
UIAlertController *privacyInfoController = [UIAlertController alertControllerWithTitle:@"温馨提示(隐私合规示例)" message:@"" preferredStyle:UIAlertControllerStyleAlert];
[privacyInfoController setValue:privacyInfo forKey:@"attributedMessage"];
UIAlertAction *agreeAllAction = [UIAlertAction actionWithTitle:@"同意(下次不提示)" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"agreeStatus"];
[[NSUserDefaults standardUserDefaults] synchronize];
//SDK. since 8.1.0
[[AMapNaviManagerConfig sharedConfig] updatePrivacyAgree:AMapPrivacyAgreeStatusDidAgree];
}];
UIAlertAction *agreeAction = [UIAlertAction actionWithTitle:@"同意" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
//SDK. since 8.1.0
[[AMapNaviManagerConfig sharedConfig] updatePrivacyAgree:AMapPrivacyAgreeStatusDidAgree];
}];
UIAlertAction *notAgreeAction = [UIAlertAction actionWithTitle:@"不同意" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"agreeStatus"];
[[NSUserDefaults standardUserDefaults] synchronize];
//SDK. since 8.1.0
[[AMapNaviManagerConfig sharedConfig] updatePrivacyAgree:AMapPrivacyAgreeStatusNotAgree];
}];
[privacyInfoController addAction:agreeAllAction];
[privacyInfoController addAction:agreeAction];
[privacyInfoController addAction:notAgreeAction];
[window.rootViewController presentViewController:privacyInfoController animated:YES completion:^{
//AppSDK. since 8.1.0
[[AMapNaviManagerConfig sharedConfig] updatePrivacyShow:AMapPrivacyShowStatusDidShow privacyInfo:AMapPrivacyInfoStatusDidContain];
}];
}
+ (void)handlePrivacyAgreeStatus {
//
// if(![[NSUserDefaults standardUserDefaults] boolForKey:@"agreeStatus"]){
//
[self showPrivacyInfoInWindow:[UIApplication sharedApplication].delegate.window];
// [[AMapNaviManagerConfig sharedConfig] updatePrivacyAgree:AMapPrivacyAgreeStatusDidAgree];
// }
}
+ (void)handlePrivacyAgreeStatusIn:(UIViewController*)targetVC {
if(![[NSUserDefaults standardUserDefaults] boolForKey:@"agreeStatus"]){
[self showPrivacyInfoInWindowWithVC:targetVC];
}
}
+ (void)showPrivacyInfoInWindowWithVC:(UIViewController *)window {
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = NSTextAlignmentLeft;
NSMutableAttributedString *privacyInfo = [[NSMutableAttributedString alloc] initWithString:@"\n感谢您一直以来的信任我们依据最新的监管要求更新了《隐私权政策》特向您说明如下\n1.为向您提供交易相关基本功能,我们会收集、使用必要的信息;\n2.基于您的明示授权,我们可能会获取您的位置(为您提供附近的店铺及优惠资讯等)等信息,您有权拒绝或取消授权;\n3.我们会采取业界先进的安全措施保护您的信息安全;\n4.未经您同意,我们不会从第三方处获取、共享或向提供您的信息;" attributes:@{
NSParagraphStyleAttributeName:paragraphStyle,
}];
[privacyInfo addAttribute:NSLinkAttributeName
value:@"《隐私权政策》"
range:[[privacyInfo string] rangeOfString:@"《隐私权政策》"]];
UIAlertController *privacyInfoController = [UIAlertController alertControllerWithTitle:@"温馨提示(隐私合规示例)" message:@"" preferredStyle:UIAlertControllerStyleAlert];
[privacyInfoController setValue:privacyInfo forKey:@"attributedMessage"];
UIAlertAction *agreeAllAction = [UIAlertAction actionWithTitle:@"同意(下次不提示)" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"agreeStatus"];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"usragreeStatus"];
[[NSUserDefaults standardUserDefaults] synchronize];
//SDK. since 8.1.0
[[AMapNaviManagerConfig sharedConfig] updatePrivacyAgree:AMapPrivacyAgreeStatusDidAgree];
[NSNotificationCenter.defaultCenter postNotificationName:@"ksAMapPrivacyDidUpdateNotification" object:nil];
}];
UIAlertAction *agreeAction = [UIAlertAction actionWithTitle:@"同意" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
//SDK. since 8.1.0
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"usragreeStatus"];
[[NSUserDefaults standardUserDefaults] synchronize];
[[AMapNaviManagerConfig sharedConfig] updatePrivacyAgree:AMapPrivacyAgreeStatusDidAgree];
[NSNotificationCenter.defaultCenter postNotificationName:@"ksAMapPrivacyDidUpdateNotification" object:nil];
}];
UIAlertAction *notAgreeAction = [UIAlertAction actionWithTitle:@"不同意" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"agreeStatus"];
[[NSUserDefaults standardUserDefaults] synchronize];
//SDK. since 8.1.0
[[AMapNaviManagerConfig sharedConfig] updatePrivacyAgree:AMapPrivacyAgreeStatusNotAgree];
}];
[privacyInfoController addAction:agreeAllAction];
[privacyInfoController addAction:agreeAction];
[privacyInfoController addAction:notAgreeAction];
[window presentViewController:privacyInfoController animated:YES completion:^{
//AppSDK. since 8.1.0
[[AMapNaviManagerConfig sharedConfig] updatePrivacyShow:AMapPrivacyShowStatusDidShow privacyInfo:AMapPrivacyInfoStatusDidContain];
}];
}
@end

View File

@@ -0,0 +1,22 @@
//
// NaviPointAnnotation.h
// AMapNaviKit
//
// Created by 刘博 on 16/3/8.
// Copyright © 2016年 AutoNavi. All rights reserved.
//
#import <AMapNaviKit/MAMapKit.h>
typedef NS_ENUM(NSInteger, NaviPointAnnotationType)
{
NaviPointAnnotationStart,
NaviPointAnnotationWay,
NaviPointAnnotationEnd
};
@interface NaviPointAnnotation : MAPointAnnotation
@property (nonatomic, assign) NaviPointAnnotationType navPointType;
@end

View File

@@ -0,0 +1,13 @@
//
// NaviPointAnnotation.m
// AMapNaviKit
//
// Created by on 16/3/8.
// Copyright © 2016 AutoNavi. All rights reserved.
//
#import "NaviPointAnnotation.h"
@implementation NaviPointAnnotation
@end

View File

@@ -0,0 +1,24 @@
//
// SelectableOverlay.h
// officialDemo2D
//
// Created by yi chen on 14-5-8.
// Copyright (c) 2014年 AutoNavi. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AMapNaviKit/MAMapKit.h>
@interface SelectableOverlay : MABaseOverlay
@property (nonatomic, assign) NSInteger routeID;
@property (nonatomic, assign, getter = isSelected) BOOL selected;
@property (nonatomic, strong) UIColor * selectedColor;
@property (nonatomic, strong) UIColor * regularColor;
@property (nonatomic, strong) id<MAOverlay> overlay;
- (id)initWithOverlay:(id<MAOverlay>) overlay;
@end

View File

@@ -0,0 +1,41 @@
//
// SelectableOverlay.m
// officialDemo2D
//
// Created by yi chen on 14-5-8.
// Copyright (c) 2014 AutoNavi. All rights reserved.
//
#import "SelectableOverlay.h"
@implementation SelectableOverlay
#pragma mark - MAOverlay Protocol
- (CLLocationCoordinate2D)coordinate
{
return [self.overlay coordinate];
}
- (MAMapRect)boundingMapRect
{
return [self.overlay boundingMapRect];
}
#pragma mark - Life Cycle
- (id)initWithOverlay:(id<MAOverlay>)overlay
{
self = [super init];
if (self)
{
self.overlay = overlay;
self.selected = NO;
self.selectedColor = [UIColor colorWithRed:0.05 green:0.39 blue:0.9 alpha:0.8];
self.regularColor = [UIColor colorWithRed:0.5 green:0.6 blue:0.9 alpha:0.8];
}
return self;
}
@end

View File

@@ -31,8 +31,12 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
# use_frameworks!
use_frameworks! :linkage => :static
pod 'AMapNavIOSSDK' , :path => './AMapNavIOSSDK'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths

View File

@@ -1,19 +1,30 @@
PODS:
- AlicloudELS (1.0.3)
- AlicloudPush (3.2.3):
- AlicloudELS (= 1.0.3)
- AlicloudELS (~> 1.0.3)
- AlicloudUTDID (~> 1.0)
- AlicloudUTDID (1.6.1)
- aliyun_push_flutter (0.0.1):
- AlicloudPush (< 4.0, >= 3.2.3)
- Flutter
- AMapFoundation-NO-IDFA (1.8.2)
- AMapLocation-NO-IDFA (2.11.0):
- AMapFoundation-NO-IDFA (>= 1.8.0)
- AMapNavi-NO-IDFA (10.1.600):
- AMapFoundation-NO-IDFA (>= 1.8.2)
- AMapNavIOSSDK (0.1.0):
- AMapLocation-NO-IDFA
- AMapNavi-NO-IDFA
- AMapSearch-NO-IDFA
- Masonry
- MJExtension
- AMapSearch-NO-IDFA (9.7.4):
- AMapFoundation-NO-IDFA (>= 1.8.0)
- connectivity_plus (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
@@ -30,19 +41,16 @@ PODS:
- FlutterMacOS
- image_picker_ios (0.0.1):
- Flutter
- Masonry (1.1.0)
- MJExtension (3.4.2)
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- saver_gallery (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -51,10 +59,10 @@ PODS:
DEPENDENCIES:
- aliyun_push_flutter (from `.symlinks/plugins/aliyun_push_flutter/ios`)
- AMapNavIOSSDK (from `./AMapNavIOSSDK`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
@@ -62,9 +70,7 @@ DEPENDENCIES:
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -75,19 +81,25 @@ SPEC REPOS:
- AlicloudELS
- AlicloudPush
trunk:
- AMapFoundation-NO-IDFA
- AMapLocation-NO-IDFA
- AMapNavi-NO-IDFA
- AMapSearch-NO-IDFA
- Masonry
- MJExtension
- OrderedSet
EXTERNAL SOURCES:
aliyun_push_flutter:
:path: ".symlinks/plugins/aliyun_push_flutter/ios"
AMapNavIOSSDK:
:path: "./AMapNavIOSSDK"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_native_splash:
@@ -102,12 +114,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
saver_gallery:
:path: ".symlinks/plugins/saver_gallery/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
@@ -115,27 +123,31 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
AlicloudELS: fbf821383330465a5af84a033f36f263ae46ca41
AlicloudPush: 95150880af380f64cf1741f5586047c17d36c1d9
AlicloudPush: 52cbf38ffc20c07f039cbc72d5738745fd986215
AlicloudUTDID: 5d2f22d50e11eecd38f30bc7a48c71925ea90976
aliyun_push_flutter: 0fc2f048a08687ef256c0cfdd72dd7a550ef3347
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
aliyun_push_flutter: ab0bf7112ef3797f506770a7a9f47f004635a9f6
AMapFoundation-NO-IDFA: 6ce0ef596d4eb8d934ff498e56747b6de1247b05
AMapLocation-NO-IDFA: 590fd42af0c8ea9eac26978348221bbc16be4ef9
AMapNavi-NO-IDFA: 22edfa7d6a81d75c91756e31b6c26b7746152233
AMapNavIOSSDK: 0cd6ec22ab6b6aba268028a5b580e18bb8066f7e
AMapSearch-NO-IDFA: 53b2193244be8f07f3be0a4d5161200236960587
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_pdfview: 2e4d13ffb774858562ffbdfdb61b40744b191adc
geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
PODFILE CHECKSUM: 357c01ff4e7591871e8c4fd6462220a8c7447220
PODFILE CHECKSUM: 97188da9dab9d4b3372eb4c16e872fbd555fdbea
COCOAPODS: 1.16.2

View File

@@ -8,11 +8,12 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
307490676CE2A16C8D75B103 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 937F9432963895EF63BCCD38 /* Pods_RunnerTests.framework */; };
298D3D45379E4332D4A8A627 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95135D36941D5EF2C00065B2 /* Pods_Runner.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
59E555C098DB12132BCE9F6E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6AF04C5CFFF0B4098EEDA799 /* Pods_Runner.framework */; };
3F21125D6B84D3CC58F3C574 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 85810788944AB2417549F45E /* Pods_RunnerTests.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
8420D0082F3D9F7E006DB6CC /* NativeFirstPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8420D0072F3D9F7E006DB6CC /* NativeFirstPage.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -49,13 +50,14 @@
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4B58A54CFC9A912F2BA04FF2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
6AF04C5CFFF0B4098EEDA799 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6D3F89E22F04C32900A154AD /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8420D0072F3D9F7E006DB6CC /* NativeFirstPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeFirstPage.swift; sourceTree = "<group>"; };
85810788944AB2417549F45E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
87773E6EB1B2C64DA1B1FA42 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
937F9432963895EF63BCCD38 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
95135D36941D5EF2C00065B2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -73,7 +75,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
59E555C098DB12132BCE9F6E /* Pods_Runner.framework in Frameworks */,
298D3D45379E4332D4A8A627 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -81,7 +83,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
307490676CE2A16C8D75B103 /* Pods_RunnerTests.framework in Frameworks */,
3F21125D6B84D3CC58F3C574 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -152,6 +154,7 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
8420D0072F3D9F7E006DB6CC /* NativeFirstPage.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
@@ -160,8 +163,8 @@
E621C70ABD0685462494972D /* Frameworks */ = {
isa = PBXGroup;
children = (
6AF04C5CFFF0B4098EEDA799 /* Pods_Runner.framework */,
937F9432963895EF63BCCD38 /* Pods_RunnerTests.framework */,
95135D36941D5EF2C00065B2 /* Pods_Runner.framework */,
85810788944AB2417549F45E /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -199,7 +202,6 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
590CF992B35CC61AF9AA4341 /* [CP] Embed Pods Frameworks */,
AF570E8AAEEA12D52BD19B4E /* [CP] Copy Pods Resources */,
);
buildRules = (
@@ -288,23 +290,6 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
590CF992B35CC61AF9AA4341 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -396,6 +381,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8420D0082F3D9F7E006DB6CC /* NativeFirstPage.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
@@ -491,7 +477,8 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO;
@@ -526,8 +513,6 @@
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"permission_handler_apple\"",
"-framework",
"\"shared_preferences_foundation\"",
@@ -565,8 +550,6 @@
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"permission_handler_apple\"",
"-framework",
"\"shared_preferences_foundation\"",
@@ -759,7 +742,8 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO;
@@ -794,8 +778,6 @@
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"permission_handler_apple\"",
"-framework",
"\"shared_preferences_foundation\"",
@@ -833,8 +815,6 @@
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"permission_handler_apple\"",
"-framework",
"\"shared_preferences_foundation\"",
@@ -864,7 +844,8 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO;
@@ -899,8 +880,6 @@
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"permission_handler_apple\"",
"-framework",
"\"shared_preferences_foundation\"",

View File

@@ -1,13 +1,86 @@
import Flutter
import UIKit
///
let kAMapKey = "key";
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
GeneratedPluginRegistrant.register(with: self)
AMapNavSDKManager.shared().config(withKey: kAMapKey)
//
let registrar = self.registrar(forPlugin: "NativeFirstPagePlugin")
let controller = window?.rootViewController as! FlutterViewController
let nativeViewFactory = NativeViewFactory(messenger: controller.binaryMessenger)
registrar?.register(
nativeViewFactory,
withId: "NativeFirstPage"
)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
//
class NativeViewFactory: NSObject, FlutterPlatformViewFactory {
private var messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) {
self.messenger = messenger
super.init()
}
func create(
withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
return NativeFlutterView(frame: frame, viewId: viewId, args: args)
}
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
return FlutterStandardMessageCodec.sharedInstance()
}
}
class NativeFlutterView: NSObject, FlutterPlatformView {
private var _view: UIView
private var _nativeVC: UIViewController
init(frame: CGRect, viewId: Int64, args: Any?) {
// ViewController
let nativeVC = AMapNavSDKManager.shared().targetVC;
// let nativeVC = NativeFirstPage();
self._nativeVC = nativeVC
print("---frame: \(frame)");
_view = nativeVC.view
_view.isUserInteractionEnabled = true
_view.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame))
super.init()
}
func view() -> UIView {
return _view
}
}

View File

@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key></key>
<string></string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
@@ -16,6 +14,11 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>zh-Hans</string>
<string>en</string>
</array>
<key>CFBundleName</key>
<string>ln_jq_app</string>
<key>CFBundlePackageType</key>
@@ -28,6 +31,10 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>iosamap</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
@@ -44,6 +51,12 @@
<string>需要访问您的相册以选择二维码图片进行识别</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
<string>location</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -63,24 +76,5 @@
</array>
<key>uses</key>
<string></string>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
</array>
<key>CFBundleLocalizations</key>
<array>
<string>zh-Hans</string>
<string>en</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<!-- 允许在“文件”App中直接打开文档 -->
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,66 @@
//
// NativeFirstPage.swift
// Runner
//
// Created by admin on 2026/2/9.
//
import UIKit
class NativeFirstPage: UIViewController {
var lable:UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = .white
// UI
let label = UILabel()
label.text = "iOS 原生页面."
label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
let button = UIButton(type: .custom)
button.setTitle("点击原生按钮", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 18)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .blue
view.addSubview(label)
view.addSubview(button)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50),
button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 30),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
self.lable = label
}
@objc func buttonTapped() {
self.lable.text = "click...";
//
let alert = UIAlertController(
title: "原生弹窗",
message: "来自 iOS 原生的提示",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "确定", style: .default))
present(alert, animated: true)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.backgroundColor = .orange
}
}

View File

@@ -1 +1,2 @@
#import "GeneratedPluginRegistrant.h"
#import <AMapNavSDKManager.h>

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get_storage/get_storage.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/AuthGuard.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/token_interceptor.dart';
import 'package:ln_jq_app/storage_service.dart';
@@ -21,7 +20,7 @@ void main() async {
logTag: '小羚羚',
supportedLocales: [const Locale('zh', 'CN')],
);
// 保持原生闪屏页,直到 WelcomeController 调用 remove()
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
@@ -66,7 +65,6 @@ void main() async {
void initHttpSet() {
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
HttpService.to.init(timeout: 15);
HttpService.to.setBaseUrl(AppTheme.test_service_url);
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
HttpService.to.setOnResponseHandler((response) async {
@@ -78,7 +76,8 @@ void initHttpSet() {
if (baseModel.code == 0 || baseModel.code == 200) {
return null;
} else if (baseModel.code == 401) {
await AuthGuard.handle401(baseModel.message);
await StorageService.to.clearLoginInfo();
// Get.offAll(() => const LoginPage());
return baseModel.message;
} else {
return (baseModel.error.toString()).isEmpty

View File

@@ -1,11 +1,10 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
class HistoryController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'history';
class HistoryController extends GetxController {
// --- 定义 API 需要的日期格式化器 ---
final DateFormat _apiDateFormat = DateFormat('yyyy-MM-dd');
@@ -14,8 +13,8 @@ class HistoryController extends GetxController with BaseControllerMixin {
final Rx<DateTime> endDate = DateTime.now().obs;
final TextEditingController plateNumberController = TextEditingController();
final RxString totalHydrogen = '0'.obs;
final RxString totalCompletions = '0'.obs;
final RxString totalHydrogen = '0 kg'.obs;
final RxString totalCompletions = '0'.obs;
final RxList<ReservationModel> historyList = <ReservationModel>[].obs;
final RxBool isLoading = true.obs;
@@ -24,31 +23,14 @@ class HistoryController extends GetxController with BaseControllerMixin {
String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value);
String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value);
String stationName = "";
final Map<String, String> statusOptions = {
'': '全部',
'100': '未预约加氢',
'0': '待加氢',
'1': '已加氢',
'2': '未加氢',
'5': '拒绝加氢',
};
final RxString selectedStatus = ''.obs;
final RxString selectedDateType = ''.obs; // week, month, three_month
@override
void onInit() {
super.onInit();
final args = Get.arguments as Map<String, dynamic>;
stationName = args['stationName'] as String? ?? "";
refreshData();
}
void refreshData() {
getAllOrderCounts();
final args = Get.arguments as Map<String, dynamic>;
stationName = args['stationName'] as String;
fetchHistoryData();
}
@@ -56,50 +38,51 @@ class HistoryController extends GetxController with BaseControllerMixin {
var response = await HttpService.to.post(
"appointment/orderAddHyd/getAllOrderCounts",
data: {
/*'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),*/
// --- 直接使用 DateFormat 来格式化日期 ---
'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),
'plateNumber': plateNumberController.text,
'stationName': stationName,
"status": selectedStatus.value,
"dateType": selectedDateType.value,
'stationName': stationName, // 加氢站名称
},
);
if (response == null || response.data == null) {
totalHydrogen.value = '0';
totalCompletions.value = '0';
totalHydrogen.value = '0 kg';
totalCompletions.value = '0';
return;
}
try {
final baseModel = BaseModel<dynamic>.fromJson(response.data);
final dataMap = baseModel.data as Map<String, dynamic>;
totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0}';
totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0}';
totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0} kg';
totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0}';
} catch (e) {
totalHydrogen.value = '0';
totalCompletions.value = '0';
totalHydrogen.value = '0 kg';
totalCompletions.value = '0';
}
}
Future<void> fetchHistoryData() async {
isLoading.value = true;
updateUi();
//获取数据
getAllOrderCounts();
try {
var response = await HttpService.to.post(
"appointment/orderAddHyd/sitOrderPage",
data: {
/*'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),*/
// --- 直接使用 DateFormat 来格式化日期 ---
'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),
'plateNumber': plateNumberController.text,
'pageNum': 1,
'pageSize': 50,
'stationName': stationName,
"status": selectedStatus.value,
"dateType": selectedDateType.value,
'stationName': stationName, // 加氢站名称
},
);
if (response == null || response.data == null) {
showToast('无法获取历史记录');
_resetData();
return;
}
@@ -107,6 +90,7 @@ class HistoryController extends GetxController with BaseControllerMixin {
final baseModel = BaseModel<dynamic>.fromJson(response.data);
if (baseModel.code == 0 && baseModel.data != null) {
final dataMap = baseModel.data as Map<String, dynamic>;
final List<dynamic> listFromServer = dataMap['records'] ?? [];
historyList.assignAll(
listFromServer
@@ -115,13 +99,14 @@ class HistoryController extends GetxController with BaseControllerMixin {
);
hasData.value = historyList.isNotEmpty;
} else {
showToast(baseModel.message);
_resetData();
}
} catch (e) {
showToast('获取历史记录失败: $e');
_resetData();
} finally {
isLoading.value = false;
updateUi();
}
}
@@ -130,15 +115,97 @@ class HistoryController extends GetxController with BaseControllerMixin {
hasData.value = false;
}
void onStatusSelected(String status) {
if (selectedStatus.value == status) return;
selectedStatus.value = status;
refreshData();
}
void pickDate(BuildContext context, bool isStartDate) {
// 确定当前操作的日期和临时存储变量
final DateTime initialDate = isStartDate ? startDate.value : endDate.value;
DateTime tempDate = initialDate;
void onDateTypeSelected(String type) {
selectedDateType.value = type;
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(
Container(
height: 300,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
// 顶部的取消和确认按钮
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消', style: TextStyle(color: Colors.grey)),
),
TextButton(
onPressed: () {
// 4. 确认后,更新对应的日期变量
if (isStartDate) {
startDate.value = tempDate;
// 如果新的开始日期晚于结束日期,自动将结束日期调整为同一天
if (tempDate.isAfter(endDate.value)) {
endDate.value = tempDate;
}
} else {
endDate.value = tempDate;
}
Get.back();
// 选择日期后自动刷新数据
fetchHistoryData();
},
child: const Text(
'确认',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
const Divider(height: 1),
// 日期选择器
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: initialDate,
// 应用动态计算好的最小/最大日期
minimumDate: minimumDate,
maximumDate: maximumDate,
onDateTimeChanged: (DateTime newDate) {
tempDate = newDate;
},
),
),
],
),
),
backgroundColor: Colors.transparent, // 使底部工作表外的区域透明
);
}
@override

View File

@@ -1,173 +1,107 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/history/controller.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
class HistoryPage extends GetView<HistoryController> {
const HistoryPage({super.key});
const HistoryPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetBuilder<HistoryController>(
init: HistoryController(),
id: 'history',
builder: (_) {
return Scaffold(
backgroundColor: const Color(0xFFF7F8FA),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20),
onPressed: () => Get.back(),
),
title: _buildSearchBox(),
),
body: Column(
children: [
_buildFilterBar(),
_buildSummaryCard(),
Expanded(child: _buildHistoryList()),
],
),
);
},
);
}
Get.put(HistoryController());
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),
return Scaffold(
appBar: AppBar(title: const Text('历史记录'), centerTitle: true),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
_buildFilterCard(context),
const SizedBox(height: 12),
_buildSummaryCard(),
const SizedBox(height: 12),
_buildListHeader(),
Expanded(child: _buildHistoryList()),
],
),
),
);
}
Widget _buildFilterBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: controller.statusOptions.entries.map((entry) {
return Obx(() {
bool isSelected = controller.selectedStatus.value == entry.key;
return GestureDetector(
onTap: () => controller.onStatusSelected(entry.key),
child: Container(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF006633) : Colors.white,
borderRadius: BorderRadius.circular(15),
),
child: Text(
entry.value,
style: TextStyle(
color: isSelected
? Colors.white
: Color.fromRGBO(148, 163, 184, 1),
fontSize: 12.sp,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w600,
),
),
),
);
});
}).toList(),
Widget _buildFilterCard(BuildContext context) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('时间范围', style: TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 8),
Row(
children: [
Expanded(child: _buildDateField(context, true)),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text(''),
),
Expanded(child: _buildDateField(context, false)),
],
),
const SizedBox(height: 16),
const Text('车牌号', style: TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 8),
SizedBox(
height: 44,
child: TextField(
controller: controller.plateNumberController,
decoration: InputDecoration(
hintText: '请输入车牌号',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
),
),
),
),
_buildTimeFilterIcon(),
],
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 _buildTimeFilterIcon() {
return PopupMenuButton<String>(
icon: LoginUtil.getAssImg("ic_ex_menu@2x"),
onSelected: controller.onDateTypeSelected,
itemBuilder: (context) => [
const PopupMenuItem(value: 'week', child: Text('最近一周')),
const PopupMenuItem(value: 'month', child: Text('最近一月')),
const PopupMenuItem(value: 'three_month', child: Text('最近三月')),
],
);
}
Widget _buildSummaryCard() {
return Container(
margin: const EdgeInsets.only(left: 16, right: 16,bottom: 12),
padding: const EdgeInsets.all(20),
height: 160,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
image: const DecorationImage(
image: AssetImage('assets/images/history_bg.png'),
fit: BoxFit.cover,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('加氢站', style: TextStyle(color: Colors.white70, fontSize: 12)),
Text(
controller.stationName,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Obx(
() => Row(
children: [
_buildSummaryItem('实际加氢量', '${controller.totalHydrogen.value} Kg'),
const SizedBox(width: 40),
_buildSummaryItem('预约完成次数', '${controller.totalCompletions.value}'),
],
),
),
],
),
);
}
Widget _buildSummaryItem(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(color: Colors.white60, fontSize: 12)),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Obx(
() => Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildSummaryItem('实际加氢总量', controller.totalHydrogen.value, Colors.blue),
const SizedBox(width: 1, height: 40, child: VerticalDivider()),
_buildSummaryItem(
'预约完成次数',
controller.totalCompletions.value,
Colors.green,
),
],
),
),
],
),
);
}
@@ -176,138 +110,143 @@ class HistoryPage extends GetView<HistoryController> {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.historyList.isEmpty) {
return const Center(
child: Text('暂无相关记录', style: TextStyle(color: Color(0xFF999999))),
);
if (!controller.hasData.value) {
return const Center(child: Text('没有找到相关记录'));
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: controller.historyList.length,
itemBuilder: (context, index) {
return _buildHistoryItem(controller.historyList[index]);
final ReservationModel item = controller.historyList[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text('车牌号: ${item.plateNumber}'),
subtitle: Text.rich(
TextSpan(
children: [
TextSpan(
text: '加氢站: ${item.stationName}\n',
style: TextStyle(fontSize: 16),
),
TextSpan(
text: '时间: ${item.time}\n',
style: TextStyle(fontSize: 16),
),
TextSpan(
text: '加氢量:',
),
TextSpan(
text: '${item.amount}',
style: TextStyle(fontSize: 16, color: AppTheme.themeColor),
),
],
),
)
,
trailing:
// 状态标签
_buildStatusChip(item.status),
),
);
},
);
});
}
Widget _buildHistoryItem(ReservationModel item) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'车牌号',
style: TextStyle(
color: Color.fromRGBO(148, 163, 184, 1),
fontSize: 12.sp,
),
),
const SizedBox(height: 4),
Text(
item.plateNumber,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
_buildStatusBadge(item.status),
],
),
const SizedBox(height: 16),
Row(
children: [
_buildInfoColumn('加氢时间:', item.time),
_buildInfoColumn('加氢量', '${item.amount} Kg', isRight: true),
],
),
],
),
);
}
Widget _buildInfoColumn(String label, String value, {bool isRight = false}) {
return Expanded(
child: Column(
crossAxisAlignment: isRight ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(color: Color.fromRGBO(148, 163, 184, 1), fontSize: 12.sp),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: isRight ? 16 : 13,
fontWeight: isRight ? FontWeight.bold : FontWeight.normal,
color: const Color(0xFF333333),
),
),
],
),
);
}
Widget _buildStatusBadge(ReservationStatus status) {
String text = '未知';
Color bgColor = Colors.grey.shade100;
Color textColor = Colors.grey;
Widget _buildStatusChip(ReservationStatus status) {
String text;
Color color;
switch (status) {
case ReservationStatus.pending:
text = '待加氢';
bgColor = const Color(0xFFFFF7E8);
textColor = const Color(0xFFFF9800);
color = Colors.orange;
break;
case ReservationStatus.completed:
text = '已加氢';
bgColor = const Color(0xFFE8F5E9);
textColor = const Color(0xFF4CAF50);
color = Colors.greenAccent;
break;
case ReservationStatus.rejected:
text = '拒绝加氢';
bgColor = const Color(0xFFFFEBEE);
textColor = const Color(0xFFF44336);
color = Colors.red;
break;
case ReservationStatus.unadded:
text = '未加氢';
bgColor = const Color(0xFFFFEBEE);
textColor = const Color(0xFFF44336);
color = Colors.red;
break;
case ReservationStatus.cancel:
text = '已取消';
bgColor = const Color(0xFFFFEBEE);
textColor = const Color(0xFFF44336);
color = Colors.red;
break;
default:
text = '未知状态';
bgColor = Colors.grey;
textColor = Colors.grey;
color = Colors.grey;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: textColor.withOpacity(0.3)),
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
text,
style: TextStyle(color: textColor, fontSize: 12, fontWeight: FontWeight.bold),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, color: color, size: 8),
const SizedBox(width: 4),
Text(
text,
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
),
],
),
);
}
Widget _buildDateField(BuildContext context, bool isStart) {
return Obx(
() => InkWell(
onTap: () => controller.pickDate(context, isStart),
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(isStart ? controller.formattedStartDate : controller.formattedEndDate),
const Icon(Icons.calendar_today, size: 18, color: Colors.grey),
],
),
),
),
);
}
Widget _buildSummaryItem(String label, String value, Color color) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(color: color, fontSize: 22, fontWeight: FontWeight.bold),
),
],
);
}
Widget _buildListHeader() {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 14.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('加氢明细', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
);
}

View File

@@ -1,13 +1,9 @@
import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/b_page/reservation/controller.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/pages/common/webview/view.dart';
class ReservationPage extends GetView<ReservationController> {
const ReservationPage({super.key});
@@ -40,31 +36,7 @@ class ReservationPage extends GetView<ReservationController> {
_buildSystemTips(),
SizedBox(height: 24),
_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: 75.h),
],
),
),
@@ -76,14 +48,6 @@ 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. 顶部个人信息及统计栏
Widget _buildTopSection(BuildContext context) {
return Container(
@@ -114,15 +78,11 @@ class ReservationPage extends GetView<ReservationController> {
children: [
Row(
children: [
Flexible(
child: Text(
controller.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
Text(
controller.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
@@ -138,11 +98,12 @@ class ReservationPage extends GetView<ReservationController> {
),
),
IconButton(
onPressed: () async {
onPressed: () async{
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
@@ -150,7 +111,9 @@ class ReservationPage extends GetView<ReservationController> {
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
@@ -269,17 +232,12 @@ class ReservationPage extends GetView<ReservationController> {
label,
style: TextStyle(color: Colors.grey, fontSize: 11.sp),
),
Expanded(
child: Text(
value,
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: const Color(0xFF333333),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
Text(
value,
style: TextStyle(
color: Color(0xFF333333),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
],
@@ -557,15 +515,14 @@ class ReservationPage extends GetView<ReservationController> {
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor,
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)),
),
),
],
),
],
),
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/history/view.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
@@ -56,33 +57,17 @@ class SitePage extends GetView<SiteController> {
),
GestureDetector(
onTap: () {
// 手动录入
controller.confirmReservation("", isAdd: true);
Get.to(
() => HistoryPage(),
arguments: {'stationName': controller.name},
);
},
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(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 13.sp,
fontWeight: FontWeight.w400
),
),
],
child: Text(
'历史记录',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: Color.fromRGBO(156, 163, 175, 1),
),
),
),
@@ -93,12 +78,12 @@ class SitePage extends GetView<SiteController> {
Column(
children: [
_buildSearchView(),
SizedBox(height: 15.h),
controller.hasReservationData
? _buildReservationListView()
: _buildEmptyReservationView(),
],
),
SizedBox(height: 35.h),
//第三部分
Container(
@@ -151,7 +136,7 @@ class SitePage extends GetView<SiteController> {
],
),
),
SizedBox(height: 105.h),
SizedBox(height: 75.h),
],
);
}
@@ -200,7 +185,27 @@ class SitePage extends GetView<SiteController> {
],
),
),
_buildDropdownMenu(),
IconButton(
onPressed: () async {
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
],
),
const SizedBox(height: 25),
@@ -228,40 +233,6 @@ 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) {
return Expanded(
child: Container(
@@ -420,9 +391,8 @@ class SitePage extends GetView<SiteController> {
/// 构建“有预约数据”的列表视图
Widget _buildReservationListView() {
return ListView.builder(
return ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
// 因为外层已有滚动,这里禁用内部滚动
itemCount: controller.reservationList.length,
@@ -431,6 +401,7 @@ class SitePage extends GetView<SiteController> {
// 调用新的方法来构建每一项
return _buildReservationItem(index, item);
},
separatorBuilder: (context, index) => const SizedBox(height: 0), // 列表项之间的间距
);
}
@@ -495,7 +466,7 @@ class SitePage extends GetView<SiteController> {
/// 右侧具体数据卡片
Widget _buildInfoCard(ReservationModel item) {
return Container(
padding: EdgeInsets.all(16),
padding: EdgeInsets.only(left: 16.w, top: 8.5, bottom: 8.5, right: 16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
@@ -537,82 +508,59 @@ class SitePage extends GetView<SiteController> {
),
const SizedBox(height: 8),
// 联系信息
Text(
item.contactPerson.isEmpty || item.contactPhone.isEmpty
? ""
: "${item.contactPerson} | ${item.contactPhone}",
style: TextStyle(
color: Color(0xFF999999),
fontSize: 13.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 6.h),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
Column(
mainAxisAlignment: MainAxisAlignment.start,
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);
},
),
Text(
"${item.contactPerson} | ${item.contactPhone}",
style: TextStyle(
color: Color(0xFF999999),
fontSize: 13.sp,
fontWeight: FontWeight.w400,
),
] else if (item.status == ReservationStatus.pending) ...[
const SizedBox(height: 15),
const Divider(height: 1, color: Color(0xFFF5F5F5)),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildSmallButton(
"拒绝",
isOutline: true,
onTap: () {
controller.rejectReservation(item.id);
},
),
const SizedBox(width: 12),
_buildSmallButton(
"确认",
isOutline: false,
onTap: () {
controller.confirmReservation(item.id);
},
),
],
),
],
),
],
),
//操作按钮(仅在待处理状态显示)
if (item.status == ReservationStatus.pending) ...[
const SizedBox(height: 15),
const Divider(height: 1, color: Color(0xFFF5F5F5)),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildSmallButton(
"拒绝",
isOutline: true,
onTap: () {
controller.rejectReservation(item.id);
},
),
const SizedBox(width: 12),
_buildSmallButton(
"确认",
isOutline: false,
onTap: () {
controller.confirmReservation(item.id);
},
),
],
),
],
],
),
);
}
/// 通用小按钮
Widget _buildSmallButton(
String text, {
required bool isOutline,
required VoidCallback onTap,
}) {
const kPrimaryGreen = Color(0xFF006D35);
var kDangerRed = text.contains('修改') ? Colors.red : Color.fromRGBO(255, 142, 98, 1);
const kDangerRed = Color(0xFFFF7D7D);
return GestureDetector(
onTap: onTap,
@@ -686,4 +634,139 @@ 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)),
],
);
}
}

View File

@@ -0,0 +1,82 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
/// 原生地图页面
class NativePageIOS extends StatelessWidget {
const NativePageIOS({super.key});
@override
Widget build(BuildContext context) {
if (Platform.isIOS) {
return _buildIOSView(context);
} else if (Platform.isAndroid) {
return _buildAndroidView(context);
} else {
return const Center(
child: Text('不支持的平台', style: TextStyle(fontSize: 16)),
);
}
}
/// 构建iOS Platform View
Widget _buildIOSView(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 100,
color: Colors.white,
child: UiKitView(
viewType: 'NativeFirstPage', // 与iOS原生端注册的标识一致
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{}.toSet(),
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
creationParamsCodec: const StandardMessageCodec(),
layoutDirection: TextDirection.ltr,
),
);
}
/// 构建Android Platform View
Widget _buildAndroidView(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 100,
color: Colors.white,
child: AndroidView(
viewType: 'NativeFirstPage', // 与Android原生端注册的标识一致
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{}.toSet(),
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
creationParamsCodec: const StandardMessageCodec(),
layoutDirection: TextDirection.ltr,
),
);
}
/// 处理点击事件(如需要)
void _handleTap(BuildContext context) {
if (kDebugMode) {
print("NativePage被点击");
}
_showDialog(context, '提示', '点击了原生地图页面');
}
/// 显示对话框
void _showDialog(BuildContext context, String title, String content) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
),
);
}
}

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/c_page/base_widgets/NativePageIOS.dart';
import 'package:ln_jq_app/pages/c_page/car_info/view.dart';
import 'package:ln_jq_app/pages/c_page/mall/mall_view.dart';
import 'package:ln_jq_app/pages/c_page/map/view.dart';
import 'package:ln_jq_app/pages/c_page/mine/view.dart';
import 'package:ln_jq_app/pages/c_page/reservation/view.dart';
@@ -34,14 +34,14 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
}
List<Widget> _buildPages() {
return [ReservationPage(), MapPage(), MallPage(), CarInfoPage(), MinePage()];
return [ReservationPage(), NativePageIOS(), CarInfoPage(), MinePage()];
}
// 自定义导航栏 (悬浮胶囊样式)
Widget _buildNavigationBar() {
return SafeArea(
child: Container(
height: Get.height * 0.05,
height: 50.h,
margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
decoration: BoxDecoration(
color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
@@ -59,9 +59,8 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
children: [
_buildNavItem(0, "ic_h2_select@2x", "ic_h2@2x"),
_buildNavItem(1, "ic_map_select@2x", "ic_map@2x"),
_buildNavItem(2, "ic_mall_select@2x", "ic_mall@2x"),
_buildNavItem(3, "ic_car_select@2x", "ic_car@2x"),
_buildNavItem(4, "ic_user_select@2x", "ic_user@2x"),
_buildNavItem(2, "ic_car_select@2x", "ic_car@2x"),
_buildNavItem(3, "ic_user_select@2x", "ic_user@2x"),
],
),
),
@@ -84,8 +83,7 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
child: SizedBox(
height: 24,
width: 24,
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),
),
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),),
),
);
}

View File

@@ -1,8 +1,5 @@
import 'package:get/get.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/vehicle_info.dart';
import 'package:ln_jq_app/pages/c_page/car_info/attachment_viewer_page.dart';
@@ -18,9 +15,9 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// --- 车辆基本信息 ---
String plateNumber = "";
String vin = "-";
String modelName = "-";
String brandName = "-";
String vin = "未知";
String modelName = "未知";
String brandName = "未知";
// --- 证件附件列表 ---
final RxList<String> drivingAttachments = <String>[].obs;
@@ -92,113 +89,6 @@ 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 {
if (StorageService.to.hasVehicleInfo) {
VehicleInfo? bean = StorageService.to.vehicleInfo;
@@ -212,7 +102,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// 获取证件信息
final response = await HttpService.to.get(
'appointment/vehicle/getPicInfoByVin?vin=$vin&plateNumber=$plateNumber',
'appointment/vehicle/getPicInfoByVin?vin=$vin',
);
if (response != null && response.data != null) {
@@ -244,10 +134,10 @@ class CarInfoController extends GetxController with BaseControllerMixin {
...registerAttachments,
];
color = data['color'].toString();
hydrogenCapacity = data['hydrogenCapacity'].toString();
rentFromCompany = data['rentFromCompany'].toString();
address = data['address'].toString();
color = data['color'].toString();
hydrogenCapacity = data['hydrogenCapacity'].toString();
rentFromCompany = data['rentFromCompany'].toString();
address = data['address'].toString();
loadAllPdfs();
}

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:image_picker/image_picker.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/storage_service.dart';
@@ -134,7 +133,7 @@ class CarInfoPage extends GetView<CarInfoController> {
),
),
IconButton(
onPressed: () async {
onPressed: () async{
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
@@ -164,26 +163,11 @@ class CarInfoPage extends GetView<CarInfoController> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildModernStatItem(
'本月里程数',
'Accumulated',
StorageService.to.hasVehicleInfo ? '2,852km' : '-',
'',
),
_buildModernStatItem('本月里程数', 'Accumulated', '2,852km', ''),
const SizedBox(width: 8),
_buildModernStatItem(
'总里程',
'Refuel Count',
StorageService.to.hasVehicleInfo ? "2.5W km" : '-',
'',
),
_buildModernStatItem('总里程', 'Refuel Count', "2.5W km", ''),
const SizedBox(width: 8),
_buildModernStatItem(
'服务评分',
'Driver rating',
StorageService.to.hasVehicleInfo ? "4.9分" : '-',
'',
),
_buildModernStatItem('服务评分', 'Driver rating', "4.9分", ''),
],
),
),
@@ -316,20 +300,20 @@ class CarInfoPage extends GetView<CarInfoController> {
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: StorageService.to.hasVehicleInfo ? 0.75 : 0,
child: const LinearProgressIndicator(
value: 0.75,
minHeight: 8,
backgroundColor: Color(0xFFF0F2F5),
valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(16, 185, 129, 1)),
),
),
const SizedBox(height: 8),
Row(
const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("H2 Level", style: TextStyle(fontSize: 11, color: Colors.grey)),
Text(
StorageService.to.hasVehicleInfo ? "75%" : "0%",
"75%",
style: TextStyle(
fontSize: 11,
color: Color.fromRGBO(16, 185, 129, 1),
@@ -369,7 +353,7 @@ class CarInfoPage extends GetView<CarInfoController> {
children: [
_buildCertificateContent('行驶证', controller.drivingAttachments),
_buildCertificateContent('营运证', controller.operationAttachments),
_buildCertificateContent('加氢证', controller.hydrogenationAttachments),
_buildCertificateContent('加氢资格', controller.hydrogenationAttachments),
_buildCertificateContent('登记证', controller.registerAttachments),
],
),
@@ -389,7 +373,7 @@ class CarInfoPage extends GetView<CarInfoController> {
child: Padding(
padding: EdgeInsets.all(16.0),
child: attachments.isEmpty
? _buildEmptyCertificateState(title)
? const Center(child: Text('暂无相关证件信息'))
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
@@ -398,11 +382,7 @@ class CarInfoPage extends GetView<CarInfoController> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildCertDetailItem(
'所属公司',
controller.rentFromCompany,
isFull: false,
),
_buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: false),
_buildCertDetailItem('运营城市', controller.address),
],
),
@@ -441,158 +421,6 @@ 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(
String label,
String value, {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,345 +0,0 @@
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,
),
),
),
),
],
),
],
),
),
],
),
);
}
}

View File

@@ -1,94 +0,0 @@
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();
}
}
}

View File

@@ -1,138 +0,0 @@
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),
),
],
),
],
),
);
}
}

View File

@@ -1,131 +0,0 @@
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),
),
),
],
),
);
}
}

View File

@@ -12,63 +12,40 @@ class MessagePage extends GetView<MessageController> {
Get.put(MessageController());
return Scaffold(
backgroundColor: const Color(0xFFF7F9FB),
appBar: AppBar(
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(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(title: const Text('消息通知'), centerTitle: true),
body: Column(
children: [
Obx(() => SmartRefresher(
controller: controller.refreshController,
enablePullUp: true,
onRefresh: controller.onRefresh,
onLoading: controller.onLoading,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
itemCount: controller.messageList.length,
itemBuilder: (context, index) {
return _buildMessageItem(context, controller.messageList[index]);
},
),
)),
Expanded(
child: Obx(() => SmartRefresher(
controller: controller.refreshController,
enablePullUp: true,
onRefresh: controller.onRefresh,
onLoading: controller.onLoading,
child: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: controller.messageList.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return _buildMessageItem(context, controller.messageList[index]);
},
),
)),
),
Obx(() => !controller.allRead.value
? Positioned(
right: 20,
bottom: 50,
child: GestureDetector(
onTap: controller.markAllRead,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
decoration: BoxDecoration(
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,
),
),
),
),
)
? Container(
padding: const EdgeInsets.all(16),
color: Colors.white,
child: ElevatedButton(
onPressed: controller.markAllRead,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
minimumSize: const Size(double.infinity, 44),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('全部标为已读', style: TextStyle(fontSize: 16, color: Colors.white)),
),
)
: const SizedBox.shrink()),
],
),
@@ -76,92 +53,54 @@ class MessagePage extends GetView<MessageController> {
}
Widget _buildMessageItem(BuildContext context, MessageModel item) {
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: () {
controller.markRead(item);
_showMessageDialog(context, item);
},
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
const SizedBox(height: 10),
Text(
item.content,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
height: 1.4,
),
),
const SizedBox(height: 12),
Text(
item.createTime,
style: const TextStyle(
fontSize: 12,
color: Color(0xFFCCCCCC),
),
),
],
),
return GestureDetector(
onTap: () {
controller.markRead(item);
_showMessageDialog(context, item);
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.only(top: 6, right: 12),
width: 8,
height: 8,
decoration: BoxDecoration(
color: item.isRead == 1 ? Colors.grey[300] : const Color(0xFFFAAD14),
shape: BoxShape.circle,
),
),
),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87),
),
const SizedBox(height: 8),
Text(
item.content,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
item.createTime,
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
),
],
),
),
],
),
),
);
}
@@ -172,7 +111,7 @@ class MessagePage extends GetView<MessageController> {
barrierDismissible: true,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
@@ -183,22 +122,22 @@ class MessagePage extends GetView<MessageController> {
item.title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const SizedBox(height: 12),
Text(
item.content,
style: const TextStyle(
fontSize: 15, height: 1.5, color: Color(0xFF333333)),
style: const TextStyle(fontSize: 15, height: 1.5, color: Colors.black87),
),
const SizedBox(height: 24),
Align(
alignment: Alignment.centerRight,
child: TextButton(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF007A45),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.blue),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
),
child: const Text('确认',
style: TextStyle(fontWeight: FontWeight.bold)),
child: const Text('确认', style: TextStyle(color: Colors.blue)),
),
),
],
@@ -208,4 +147,4 @@ class MessagePage extends GetView<MessageController> {
},
);
}
}
}

View File

@@ -1,15 +1,10 @@
import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/common/index.dart';
import 'package:getx_scaffold/common/widgets/index.dart';
import 'package:ln_jq_app/common/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/common/webview/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import 'controller.dart';
@@ -43,30 +38,6 @@ class MinePage extends GetView<MineController> {
_buildSafetyReminderCard(),
SizedBox(height: 24.h),
_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),
],
),
@@ -80,15 +51,6 @@ 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() {
return Card(

View File

@@ -69,34 +69,239 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
String get formattedTimeSlot =>
'${_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() {
final now = DateTime.now();
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)) {
// 如果是今天:起始时间 = max(当前小时, 营业开始小时),且上限为营业结束小时
int targetHour = now.hour;
if (targetHour < bizStartHour) targetHour = bizStartHour;
if (targetHour > bizEndHour) targetHour = bizEndHour;
startTime.value = TimeOfDay(hour: targetHour, minute: 0);
// 如果是今天,就将时间重置为当前时间所在的半小时区间
startTime.value = _calculateInitialStartTime(now);
endTime.value = TimeOfDay.fromDateTime(
_getDateTimeFromTimeOfDay(startTime.value).add(const Duration(minutes: 30)),
);
} else {
// 如果是明天:起始时间直接重置为营业开始小时
startTime.value = TimeOfDay(hour: bizStartHour, minute: 0);
// 如果是明天(或其他未来日期),则可以将时间重置为一天的最早可用时间,例如 00:00
startTime.value = const TimeOfDay(hour: 0, minute: 0);
endTime.value = const TimeOfDay(hour: 0, minute: 30);
}
}
///60 分钟为间隔 时间选择器
void pickTime(BuildContext context) {
final now = DateTime.now();
final isToday =
selectedDate.value.year == now.year &&
selectedDate.value.month == now.month &&
selectedDate.value.day == now.day;
final List<TimeSlot> availableSlots = [];
for (int i = 0; i < 24; i++) {
// 每次增加 60 分钟
final startMinutes = i * 60;
final endMinutes = startMinutes + 60;
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
// 注意endMinutes % 60 始终为 0因为间隔是整小时
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
// 如果不是今天,所有时间段都有效
if (!isToday) {
availableSlots.add(TimeSlot(startTime, endTime));
} else {
// 如果是今天,需要判断该时间段是否可选
// 创建时间段的结束时间对象
final slotEndDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.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));
}
}
}
// 结束时间默认顺延1小时
endTime.value = TimeOfDay(hour: (startTime.value.hour + 1) % 24, minute: 0);
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,
);
}
// 用于存储上一次成功预约的信息
@@ -221,14 +426,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
stateName: '',
addStatus: '',
addStatusName: '',
hasEdit: true,
rejectReason: '',
isTruckAttachment: 0,
hasHydrogenationAttachment: true,
hasDrivingAttachment: true,
isEdit: '',
drivingAttachments: [],
hydrogenationAttachments: [], gunNumber: '',
hasEdit: true,
);
//打开预约列表
@@ -259,7 +458,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
if (_debounce?.isActive ?? false) {
return;
}
_debounce = Timer(const Duration(milliseconds: 200), () {});
_debounce = Timer(const Duration(seconds: 1), () {});
showLoading("加载中");
@@ -337,13 +536,12 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
}
}
String workEfficiency = "-";
String fillingWeight = "-";
String fillingTimes = "-";
String modeImage = "";
String workEfficiency = "0";
String fillingWeight = "0";
String fillingTimes = "0";
String plateNumber = "";
String vin = "";
String leftHydrogen = "-";
String leftHydrogen = "0";
num maxHydrogen = 0;
String difference = "";
var progressValue = 0.0;
@@ -399,7 +597,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
// 创建一个每1分钟执行一次的周期性定时器
_refreshTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
getSiteList(showloading: false);
getSiteList();
});
}
@@ -452,7 +650,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
try {
HttpService.to.setBaseUrl(AppTheme.test_service_url);
var responseData = await HttpService.to.get(
'appointment/truck/history-filling-summary?vin=$vin&plateNumber=$plateNumber',
'appointment/truck/history-filling-summary?vin=$vin',
);
if (responseData == null || responseData.data == null) {
showToast('服务暂不可用,请稍后');
@@ -466,7 +664,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
fillingWeight = "$formatted${result.data["fillingWeightUnit"]}";
fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}";
modeImage = result.data["modeImage"].toString();
updateUi();
} catch (e) {
@@ -492,8 +689,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
var result = BaseModel.fromJson(responseData.data);
leftHydrogen = "${result.data["leftHydrogen"]}Kg";
workEfficiency = "${result.data["workEfficiency"]}Kg";
leftHydrogen = result.data["leftHydrogen"].toString();
workEfficiency = result.data["workEfficiency"].toString();
final leftHydrogenNum = double.tryParse(leftHydrogen) ?? 0.0;
difference = (maxHydrogen - leftHydrogenNum).toStringAsFixed(2);
@@ -527,7 +724,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
updateUi();
}
void getSiteList({showloading = true}) async {
void getSiteList() async {
if (StorageService.to.phone == "13344444444") {
//该账号给stationOptions手动添加一个数据
final testStation = StationModel(
@@ -538,9 +735,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
// 价格
siteStatusName: '营运中',
// 状态
isSelect: 1,
startBusiness: '08:00:00',
endBusiness: '20:00:00', // 默认可选
isSelect: 1, // 默认可选
);
// 使用 assignAll 可以确保列表只包含这个测试数据
stationOptions.assignAll([testStation]);
@@ -552,9 +747,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
}
try {
if (showloading) {
showLoading("加氢站数据加载中");
}
showLoading("加氢站数据加载中");
var responseData = await HttpService.to.get(
"appointment/station/queryHydrogenSiteInfo",

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/common/index.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/c_page/reservation/controller.dart';
import 'package:ln_jq_app/pages/c_page/reservation_edit/controller.dart';
import 'package:ln_jq_app/pages/c_page/reservation_edit/view.dart';
@@ -37,19 +36,19 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
@override
Widget build(BuildContext context) {
return Container(
height: Get.height * 0.6,
height: Get.height * 0.55,
decoration: const BoxDecoration(
color: Color.fromRGBO(247, 249, 251, 1),
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 构建标题和下拉框
_buildHeader(),
const Divider(height: 1),
// 下拉筛选框
_buildChoice(),
// 构建列表(使用 Obx 监听数据变化)
@@ -59,64 +58,60 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
);
}
Widget _buildChoice() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
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(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
decoration: BoxDecoration(
// 选中色使用深绿色,未选中保持纯白
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,
),
),
Container _buildChoice() {
return Container(
padding: const EdgeInsets.fromLTRB(20, 8, 0, 0),
alignment: AlignmentGeometry.centerLeft,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
);
}).toList(),
),
);
child: DropdownButton<String>(
value: _selectedStatus,
underline: const SizedBox.shrink(), // 隐藏下划线
items: _statusOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(entry.value),
);
}).toList(),
onChanged: (newValue) {
if (newValue != null) {
setState(() {
_selectedStatus = newValue;
});
// 当选择新状态时,调用接口刷新数据
_controller.getReservationList(addStatus: _selectedStatus);
}
},
),
),
);
}
/// 构建标题、关闭按钮和下拉筛选框
Widget _buildHeader() {
return Container(
margin: const EdgeInsets.fromLTRB(20, 20, 8, 8),
child: const Text(
'我的预约',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
padding: const EdgeInsets.fromLTRB(20, 8, 8, 8),
child: Stack(
alignment: Alignment.center,
children: [
Center(child: const Text('我的预约', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
),
child: const Text('关闭', style: TextStyle(color: Colors.black54)),
),
),
],
),
);
}
@@ -139,8 +134,8 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
return Card(
color: Colors.white,
margin: const EdgeInsets.only(bottom: 12.0),
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -156,20 +151,17 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
vertical: 5,
),
decoration: BoxDecoration(
color: reservation.state == "-1"
? Color.fromRGBO(241, 67, 56, 0.1)
: Color.fromRGBO(230, 249, 243, 1),
color: Colors.blue.shade50, // 淡蓝色背景
borderRadius: BorderRadius.circular(4), // 小圆角
// 可以选择去掉边框,或者用极淡的边框
border: Border.all(color: Colors.blue.shade100),
),
child: Text(
"${reservation.stateName}-${reservation.addStatusName}",
style: TextStyle(
color: reservation.state == "-1"
? Color.fromRGBO(241, 67, 56, 0.8)
: Color.fromRGBO(49, 186, 133, 1),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
@@ -179,10 +171,12 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
SizedBox(
height: 28, // 限制按钮高度,显得精致
child: OutlinedButton(
onPressed: () async {
onPressed: () async{
var responseData = await HttpService.to.post(
'appointment/orderAddHyd/vehicle-cancel',
data: {'id': reservation.id},
data: {
'id': reservation.id,
},
);
if (responseData == null || responseData.data == null) {
@@ -204,27 +198,24 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12),
side: BorderSide(color: Colors.grey.shade400),
shape: const StadiumBorder(),
side: BorderSide(color: Colors.grey.shade400), // 灰色边框
shape: const StadiumBorder(), // 胶囊形状
),
child: Text(
'取消预约',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 11.sp,
),
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
),
),
),
SizedBox(width: 10.w),
SizedBox(width: 10.w,),
// 修改按钮 (仅在 hasEdit 为 true 时显示)
if (reservation.hasEdit)
SizedBox(
height: 28,
child: OutlinedButton(
onPressed: () async {
onPressed: () async{
var result = await Get.to(
() => ReservationEditPage(),
() => ReservationEditPage(),
arguments: {
'reservation': reservation,
'difference': _controller.difference,
@@ -242,22 +233,21 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12),
side: BorderSide(color: AppTheme.themeColor),
side: const BorderSide(color: Colors.blue), // 蓝色边框
shape: const StadiumBorder(),
backgroundColor: Colors.white,
),
child: Text(
child: const Text(
'修改',
style: TextStyle(color: AppTheme.themeColor, fontSize: 11.sp),
style: TextStyle(color: Colors.blue, fontSize: 12),
),
),
),
),),
],
),
const SizedBox(height: 12),
_buildDetailRow('车牌号:', reservation.plateNumber),
_buildDetailRow('预约日期:', reservation.date),
_buildDetailRow('预约氢量:', "${reservation.hydAmount} KG"),
_buildDetailRow('预约氢量:', reservation.hydAmount),
_buildDetailRow('加氢站:', reservation.stationName),
_buildDetailRow('开始时间:', reservation.startTime),
_buildDetailRow('结束时间:', reservation.endTime),
@@ -281,26 +271,11 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
Text(label, style: const TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
Text(
value,
style: TextStyle(
color: label.contains("预约氢量:")
? Color.fromRGBO(27, 168, 85, 1)
: Color.fromRGBO(51, 51, 51, 1),
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
Expanded(
child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
),
],
),

View File

@@ -49,7 +49,7 @@ class ReservationPage extends GetView<C_ReservationController> {
Positioned(
left: 20.w,
right: 20.w,
bottom: Get.height * (Get.height < 826 ? 0.08 : 0.11),
bottom: 110.h,
child: _buildReservationItem(context),
),
],
@@ -148,11 +148,9 @@ class ReservationPage extends GetView<C_ReservationController> {
),
IconButton(
onPressed: () async {
controller.stopAutoRefresh();
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
controller.startAutoRefresh();
}
},
icon: Badge(
@@ -179,12 +177,7 @@ class ReservationPage extends GetView<C_ReservationController> {
const SizedBox(width: 8),
_buildModernStatItem('总加氢次数', '', controller.fillingTimes, ''),
const SizedBox(width: 8),
_buildModernStatItem(
'今日里程',
'',
StorageService.to.hasVehicleInfo ? "7kg" : "-",
'',
),
_buildModernStatItem('今日里程', '', "7kg", ''),
],
),
),
@@ -244,33 +237,17 @@ class ReservationPage extends GetView<C_ReservationController> {
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
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'));
},
),
),
Expanded(flex: 4, child: LoginUtil.getAssImg('ic_car_bg@2x')),
const SizedBox(width: 16),
Expanded(
flex: 6,
child: Column(
children: [
_buildCarDataItem(
'剩余电量',
StorageService.to.hasVehicleInfo ? '36.8%' : '-',
),
_buildCarDataItem('剩余电量', '36.8%'),
const SizedBox(height: 8),
_buildCarDataItem('剩余氢量', controller.leftHydrogen),
_buildCarDataItem('剩余氢量', '${controller.leftHydrogen}Kg'),
const SizedBox(height: 8),
_buildCarDataItem('百公里氢耗', controller.workEfficiency),
_buildCarDataItem('百公里氢耗', '${controller.workEfficiency}Kg'),
const SizedBox(height: 12),
Column(
children: [
@@ -298,7 +275,7 @@ class ReservationPage extends GetView<C_ReservationController> {
),
),
Text(
controller.leftHydrogen,
"${controller.leftHydrogen}Kg",
style: const TextStyle(
fontSize: 10,
color: Color(0xFF006633),
@@ -457,34 +434,8 @@ class ReservationPage extends GetView<C_ReservationController> {
/// 时间 Slider 选择器
Widget _buildTimeSlider(BuildContext context) {
return Obx(() {
// 获取站点信息
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;
// 这里的逻辑对应 Controller 中的 24 小时可用 Slot
int currentIdx = controller.startTime.value.hour;
return Column(
children: [
@@ -533,20 +484,23 @@ class ReservationPage extends GetView<C_ReservationController> {
overlayColor: const Color(0xFF006633).withOpacity(0.1),
),
child: Slider(
value: sliderValue,
min: minVal,
max: maxVal,
// divisions: bizEndHour - bizStartHour > 0 ? bizEndHour - bizStartHour : 1,
value: currentIdx.toDouble(),
min: 0,
max: 23,
divisions: 23,
onChanged: (val) {
int hour = val.toInt();
// 模拟 Controller 中的 pickTime 逻辑校验
final now = DateTime.now();
final isToday =
controller.selectedDate.value.year == now.year &&
controller.selectedDate.value.month == now.month &&
controller.selectedDate.value.day == now.day;
// 如果是今天,判断不可选过去的时间点
if (isToday && hour < now.hour) return;
if (isToday && hour < now.hour) {
// 如果是今天且小时数小于当前,则忽略
return;
}
controller.startTime.value = TimeOfDay(hour: hour, minute: 0);
controller.endTime.value = TimeOfDay(hour: (hour + 1) % 24, minute: 0);
@@ -673,7 +627,6 @@ class ReservationPage extends GetView<C_ReservationController> {
onChanged: (value) {
if (value != null) {
controller.selectedStationId.value = value;
controller.resetTimeForSelectedDate();
}
},

View File

@@ -2,11 +2,8 @@ import 'dart:io';
import 'package:aliyun_push_flutter/aliyun_push_flutter.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: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/pages/b_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
@@ -23,200 +20,108 @@ class HomeController extends GetxController with BaseControllerMixin {
final _aliyunPush = AliyunPushFlutter();
@override
bool get listenLifecycleEvent => true;
@override
void onInit() {
super.onInit();
// 检查是否同意过隐私政策,只有同意后才初始化推送
if (StorageService.to.isPrivacyAgreed) {
requestPermission();
initAliyunPush();
addPushCallback();
}
initAliyunPush();
addPushCallback();
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() {
requestPermission();
//登录状态跳转
if (StorageService.to.isLoggedIn) {
// 如果已登录,再判断是哪个渠道
if (StorageService.to.loginChannel == LoginChannel.station) {
return B_BaseWidgetsPage();
return B_BaseWidgetsPage(); // 站点首页
} else if (StorageService.to.loginChannel == LoginChannel.driver) {
return BaseWidgetsPage();
return BaseWidgetsPage(); // 司机首页
} else {
return LoginPage();
}
} else {
return LoginPage();
// 未登录,直接去登录页
return BaseWidgetsPage();
// return LoginPage();
}
}
void requestPermission() async {
PermissionStatus status = await Permission.notification.status;
if (status.isGranted) return;
if (status.isGranted) {
Logger.d("通知权限已开启");
return;
}
if (status.isDenied) {
// 建议此处增加一个应用内的 Rationale (解释说明) 弹窗
status = await Permission.notification.request();
}
if (status.isGranted) {
// 授权成功
Logger.d('通知已开启');
} else if (status.isPermanentlyDenied) {
Logger.d('通知权限已被拒绝,请到系统设置中开启');
} else if (status.isDenied) {
Logger.d('请授予通知权限,以便接收加氢站通知');
}
}
///推送相关初始化 (保持原样)
///推送相关
Future<void> initAliyunPush() async {
// 1. 配置分离:建议将 Key 提取到外部或配置文件中
final String appKey = Platform.isIOS ? AppTheme.ios_key : AppTheme.android_key;
final String appSecret = Platform.isIOS
? AppTheme.ios_appsecret
: AppTheme.android_appsecret;
try {
// 初始化推送
final result = await _aliyunPush.initPush(appKey: appKey, appSecret: appSecret);
if (result['code'] != kAliyunPushSuccessCode) return;
if (result['code'] != kAliyunPushSuccessCode) {
Logger.d('初始化推送失败: ${result['code']} - ${result['errorMsg']}');
return;
}
Logger.d('阿里云推送初始化成功');
// 分平台配置
if (Platform.isIOS) {
await _setupIOSConfig();
} else if (Platform.isAndroid) {
await _setupAndroidConfig();
}
} catch (e) {
Logger.d('初始化异常: $e');
Logger.d('初始化过程中发生异常: $e');
}
}
/// iOS 专属配置
Future<void> _setupIOSConfig() async {
await _aliyunPush.showIOSNoticeWhenForeground(true);
final res = await _aliyunPush.showIOSNoticeWhenForeground(true);
if (res['code'] == kAliyunPushSuccessCode) {
Logger.d('iOS 前台通知展示已开启');
} else {
Logger.d('iOS 前台通知开启失败: ${res['errorMsg']}');
}
}
/// Android 专属配置
Future<void> _setupAndroidConfig() async {
await _aliyunPush.setNotificationInGroup(true);
await _aliyunPush.createAndroidChannel(
final res = await _aliyunPush.createAndroidChannel(
"xll_push_android",
'新消息通知',
4,
'用于接收加氢站实时状态提醒',
);
if (res['code'] == kAliyunPushSuccessCode) {
Logger.d('Android 通知通道创建成功');
} else {
Logger.d('Android 通道创建失败: ${res['code']} - ${res['errorMsg']}');
}
}
void addPushCallback() {
@@ -235,23 +140,40 @@ class HomeController extends GetxController with BaseControllerMixin {
Future<void> _onAndroidNotificationClickedWithNoAction(
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 {
Logger.d('onNotificationOpened ====> $message');
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');
}
}

View File

@@ -5,13 +5,18 @@ import 'package:ln_jq_app/pages/home/controller.dart';
class HomePage extends GetView<HomeController> {
const HomePage({super.key});
// 主视图
Widget _buildView() {
return <Widget>[Text('主页面')].toColumn(mainAxisSize: MainAxisSize.min).center();
}
@override
Widget build(BuildContext context) {
return GetBuilder<HomeController>(
init: HomeController(),
id: 'home',
builder: (_) {
return Scaffold(body: controller.getHomePage());
return controller.getHomePage();
},
);
}

View File

@@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.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/model/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart';
@@ -18,8 +17,6 @@ import 'package:ln_jq_app/pages/login/controller.dart';
import 'package:ln_jq_app/pages/url_host/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import '../c_page/message/view.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@@ -33,7 +30,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
bool _obscureText = true;
bool _rememberPassword = true;
bool _credentialsLoaded = false;
bool isPushInitialized = false;
@override
void initState() {
@@ -150,8 +146,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
const SizedBox(height: 10),
buildAgreement(),
const SizedBox(height: 80),
_buildFooterSlogan()
const SizedBox(height: 40),
],
),
),
@@ -160,6 +155,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
),
),
),
Positioned(left: 0, right: 0, bottom: 33.h, child: _buildFooterSlogan()),
if (AppTheme.is_show_host)
Positioned(
top: 40.h,
@@ -208,9 +204,9 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
Row(
children: [
const Text(
"欢迎使用小羚羚 ",
"欢迎使用 ",
style: TextStyle(
fontSize: 22,
fontSize: 24,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(51, 51, 51, 1),
),
@@ -392,28 +388,13 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
content: _buildDialogContent(),
confirmText: '同意',
cancelText: '拒绝',
onConfirm: () async {
onConfirm: () {
_isAgreed = true;
controller.updateUi();
// 保存隐私政策同意状态
await StorageService.to.savePrivacyAgreed(true);
// 申请通知权限
await _requestNotificationPermission();
// 初始化阿里云推送
await _initPushService();
},
);
return;
}
// 如果已经同意过,但推送还没初始化,则初始化
if (!isPushInitialized) {
await _initPushService();
}
_tabController.index == 0
? _handleDriverLogin(controller)
: _handleStationLogin(controller);
@@ -555,62 +536,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
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();
void addAlias(String alias) async {

View File

@@ -23,7 +23,7 @@ class WelcomePage extends GetView<WelcomeController> {
right: 0,
child: Image.asset(
'assets/images/welcome.png',
fit: BoxFit.cover
fit: BoxFit.fill
),
),
],

View File

@@ -25,14 +25,11 @@ class StorageService extends GetxService {
static const String _stationAccountKey = 'station_account';
static const String _stationPasswordKey = 'station_password';
// 新增:用于标记绑定车辆”弹窗是否已在本会话中显示过
// 新增:用于标记绑定车辆”弹窗是否已在本会话中显示过
static const String _bindDialogShownKey = 'bind_vehicle_dialog_shown';
static const String _hostUrlKey = 'host_url';
// 隐私政策相关
static const String _privacyAgreedKey = 'privacy_agreed';
static StorageService get to => Get.find();
Future<StorageService> init() async {
@@ -66,12 +63,9 @@ class StorageService extends GetxService {
String? get stationPassword => _box.read<String?>(_stationPasswordKey);
// 新增:获取绑定车辆”弹窗是否已显示的标志
// 新增:获取绑定车辆”弹窗是否已显示的标志
bool get hasShownBindVehicleDialog => _box.read<bool>(_bindDialogShownKey) ?? false;
// 获取隐私政策是否已同意
bool get isPrivacyAgreed => _box.read<bool>(_privacyAgreedKey) ?? false;
VehicleInfo? get vehicleInfo {
final vehicleJson = _box.read<String?>(_vehicleInfoKey);
if (vehicleJson != null) {
@@ -116,11 +110,6 @@ class StorageService extends GetxService {
await _box.write(_stationPasswordKey, password);
}
// 保存隐私政策同意状态
Future<void> savePrivacyAgreed(bool agreed) async {
await _box.write(_privacyAgreedKey, agreed);
}
// 新增:标记“绑定车辆”弹窗已显示
Future<void> markBindVehicleDialogAsShown() async {
await _box.write(_bindDialogShownKey, true);

File diff suppressed because it is too large Load Diff

View File

@@ -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
# 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.
version: 1.2.4+7
version: 1.2.3+6
environment:
sdk: ^3.9.0
# Dependencies specify other packages tha。。。t your package needs in order to work.
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
@@ -52,9 +52,8 @@ dependencies:
geolocator: ^14.0.2 # 获取精确定位
aliyun_push_flutter: ^1.3.6
pull_to_refresh: ^2.0.0
flutter_app_update: ^3.2.2
saver_gallery: ^4.0.0
path_provider: ^2.1.5
dev_dependencies:
flutter_test:
@@ -65,7 +64,6 @@ dev_dependencies:
dependency_overrides:
intl: 0.19.0
device_info_plus: ^12.3.0
flutter: