29 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
35bd3a78a5 Merge branch 'dev' 2026-01-30 17:33:15 +08:00
1278c38b7e 地图样式调整 2026-01-30 17:32:54 +08:00
032e60d362 Merge branch 'dev'
ui调整
# Conflicts:
#	ln_jq_app/lib/pages/login/view.dart
2026-01-30 17:08:49 +08:00
171f556b40 viersion 1.2.3 2026-01-30 16:06:58 +08:00
55eade54b6 问题修改 2026-01-30 16:02:35 +08:00
bc99ffd691 增加参数 2026-01-30 13:01:50 +08:00
aa52a56bcf 地图相关的修改 2026-01-30 11:36:33 +08:00
73343ca297 调整 2026-01-30 10:01:31 +08:00
d09faac1d2 调整 2026-01-29 19:26:59 +08:00
1177be821a 样式调整 2026-01-29 17:01:21 +08:00
e59b89c225 Merge branch 'dev_feature' into dev
ui调整
# Conflicts:
#	ln_jq_app/lib/pages/b_page/reservation/controller.dart
#	ln_jq_app/lib/pages/b_page/site/controller.dart
#	ln_jq_app/lib/pages/b_page/site/view.dart
#	ln_jq_app/lib/pages/c_page/mine/view.dart
#	ln_jq_app/lib/pages/c_page/reservation/controller.dart
#	ln_jq_app/lib/pages/c_page/reservation/view.dart
#	ln_jq_app/lib/pages/login/view.dart
2026-01-29 11:45:17 +08:00
79fe3257b5 状态样式 2026-01-28 17:59:26 +08:00
55569839a7 搜索框 2026-01-28 16:27:37 +08:00
7112d70aba 站点样式 2026-01-28 15:00:30 +08:00
f8a8ecb0ed 司机 预约 样式修改 2026-01-27 17:35:37 +08:00
18c04272e2 车辆信息的部分 2026-01-27 13:57:14 +08:00
14e7fb3d78 首页ui 2026-01-27 13:15:53 +08:00
5ffaf81223 欢迎页 登录 2026-01-27 09:21:36 +08:00
907983a1d1 登录修改 2026-01-26 14:07:58 +08:00
9fdca9136d 导航栏 2026-01-23 17:00:17 +08:00
16bae6a1e9 车辆信息 ui 2026-01-23 15:02:16 +08:00
aabfbfae0c 车辆样式调整 2026-01-23 10:49:02 +08:00
5236670e7c ui修改 2026-01-23 09:22:07 +08:00
cf3ad579d3 新ui调整 2026-01-22 17:29:54 +08:00
70a752b6e5 协议同意 2026-01-21 14:29:03 +08:00
100 changed files with 7620 additions and 2847 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 = 5
versionName = "1.2.2"
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

@@ -14,7 +14,12 @@
<!--定位权限-->
<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"/>
<!--用于申请调用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"/>
@@ -22,6 +27,19 @@
android:label="小羚羚"
android:name="${applicationName}"
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:exported="true"

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -25,6 +25,41 @@
display: none !important;
}
/* 去除高德默认的 label 边框和背景 */
.amap-marker-label {
border: none !important;
background-color: transparent !important;
}
/* 自定义气泡样式 */
.custom-bubble {
position: relative;
background: rgba(51, 51, 51, 0.7);
/* #33333399 对应 rgba(51,51,51,0.7) */
color: #fff;
padding: 6px 15px;
border-radius: 20px;
/* 圆角 */
font-size: 13px;
white-space: nowrap;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
text-align: center;
line-height: 1.4;
}
/* 气泡下方的向下小箭头 */
.custom-bubble::after {
content: '';
position: absolute;
bottom: -6px;
/* 箭头高度 */
left: 50%;
transform: translateX(-50%);
border-width: 6px 6px 0 6px;
border-style: solid;
border-color: rgba(51, 51, 51, 0.7) transparent transparent transparent;
}
#panel .amap-call {
display: none;
}
@@ -32,7 +67,7 @@
/* --- 搜索栏样式 --- */
#search-box {
position: absolute;
top: 10px;
top: 40px;
left: 10px;
right: 10px;
z-index: 100;
@@ -63,7 +98,7 @@
button {
padding: 0 15px;
height: 38px;
background: #3366FF;
background: #017143FF;
color: #fff;
border: none;
border-radius: 4px;
@@ -74,7 +109,7 @@
/* --- 导航结果面板 (底部弹出) --- */
#panel {
position: fixed;
bottom: 0;
bottom: 75px;
left: 0;
width: 100%;
height: 35%;
@@ -94,7 +129,7 @@
#location-btn {
position: fixed;
right: 10px;
bottom: 50px;
bottom: 75px;
/* 默认位置 */
width: 44px;
height: 44px;
@@ -121,9 +156,24 @@
fill: #555;
}
/* --- 调整比例尺位置 --- */
.amap-scalecontrol {
/* 初始状态:避开底部的定位按钮或留出安全间距 */
bottom: 80px !important;
left: 10px !important;
transition: bottom 0.3s ease;
/* 增加平滑动画 */
}
/* --- 当路径规划面板显示时,比例尺自动上移 --- */
body.panel-active .amap-scalecontrol {
bottom: 38% !important;
/* 移动到面板上方 (面板高度35% + 3%间距) */
}
/* --- 关键:当 body 有 panel-active 类时,按钮上移 --- */
body.panel-active #location-btn {
bottom: 38%;
bottom: 45%;
/* 对应 #panel 的 height + 一点间距 */
}
</style>
@@ -167,7 +217,7 @@
</div>
<script>
var map, marker, driving, truckDriving, geocoder;
var map, marker, destMarker, driving, truckDriving, geocoder;
var currentLat, currentLng;
var isTruckMode = false;
var isInitialLocationSet = false;
@@ -176,7 +226,7 @@
function initMap() {
map = new AMap.Map('container', {
resizeEnable: true,
zoom: 15,
zoom: 17,
viewMode: '3D'
});
@@ -237,7 +287,7 @@
/**
* 核心功能 1: 接收 Flutter 传来的定位数据
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
*
*
*/
function updateMyLocation(lat, lng, angle) {
var rawLat = parseFloat(lat);
@@ -263,6 +313,7 @@
angle: isNaN(rawAngle) ? 0 : rawAngle,
});
map.setCenter(position);
map.setZoom(13);
} else {
marker.moveTo(position, {
duration: 1000,
@@ -281,6 +332,10 @@
const addressComponent = regeo.addressComponent;
const pois = regeo.pois;
console.log("地理:" + JSON.stringify(result));
fetchStationInfo(addressComponent.province, addressComponent.city,
addressComponent.district, lat, lng);
// 策略1: 优先使用最近的、类型合适的POI的名称
if (pois && pois.length > 0) {
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI这类POI通常是具体的建筑或地点名
@@ -295,8 +350,9 @@
}
// 策略2: 如果没有POI使用"道路+门牌号"
else if (addressComponent.street && addressComponent.streetNumber) {
shortAddress = addressComponent.street + addressComponent
.streetNumber;
shortAddress = addressComponent.district +
addressComponent.township +
addressComponent.street + addressComponent.streetNumber;
}
// 策略3: 如果还没有,使用"区+乡镇"
else if (addressComponent.district) {
@@ -330,7 +386,104 @@
}
/**
* 核心功能 2: 点击按钮回到当前位置
* 调用后端接口获取站点
*/
function fetchStationInfo(province, city, district, lat, lng) {
// 注意:某些直辖市在高德中 city 字段可能为空,需做兼容处理
console.log("JS->: 开始请求." + province + city + district);
var cityName = (typeof city === 'string' && city.length > 0) ? city : province;
fetch('https://beta-esg.api.lnh2e.com/appointment/station/getStationInfoByArea', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// "asoco-token": "e28eada8-4611-4dc2-a942-0122e52f52da"
},
body: JSON.stringify({
province: province,
city: cityName,
district: district,
longitude: lng,
latitude: lat,
})
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误: ' + response.status);
}
return response.json(); // 解析 JSON
})
.then(res => {
// 打印完整的返回结果,方便调试观察结构
console.log("JS->: 接口完整返回:", JSON.stringify(res));
// 安全校验:判断 res.data 是否存在
if (res.code === 0 && res.data) {
if (res.data.address) {
console.log("JS->: 找到地址:", res.data.address);
var destAddress = res.data.address;
document.getElementById('endInput').value = destAddress;
// 标记终点
markDestination(destAddress, res.data.name || "目的地",
res.data.longitude, res.data.latitude
);
} else {
console.log("JS->: 接口请求成功,但该区域暂无站点地址");
}
} else {
console.log("JS->: 业务报错或无数据:", res.message);
}
})
.catch(err => console.error('JS->:获取站点信息失败:', err));
}
/**
* 地理编码并在地图标记终点
*/
function markDestination(address, name, longitude, latitude) {
// 1. 清除旧的终点标记
if (destMarker) destMarker.setMap(null);
// 2. 创建自定义图标
// 假设图标大小为 32x32你可以根据实际图片尺寸调整 Size
var destIcon = new AMap.Icon({
size: new AMap.Size(32, 32), // 图标尺寸
image: 'ic_tag.png', // 本地图片路径
imageSize: new AMap.Size(32, 32) // 图片在图标内拉伸的大小
});
// 3. 创建标记
destMarker = new AMap.Marker({
map: map,
position: [longitude, latitude],
icon: destIcon, // 使用自定义图标
// 偏移量如果图标底部中心是尖角offset 设为宽的一半的负数,高度的负数
// 这样能确保图片的底部尖端指向地图上的精确位置
offset: new AMap.Pixel(-16, -32),
title: name,
label: {
content: '<div class="custom-bubble">' + name + '</div>',
direction: 'top'
}
});
// 4. 打印调试信息
console.log("JS->: 终点标记已添加", address, loc.toString());
// 5. 自动调整视野包含起点和终点
// if (marker) {
// // 如果起点标志已存在,缩放地图以展示两者
// map.setFitView([marker, destMarker], false, [60, 60, 60, 60]);
// } else {
// // 如果没有起点,直接跳到终点
// map.setCenter(loc);
// }
}
/**
* 点击按钮回到当前位置
*/
function backToLocation() {
if (currentLng && currentLat) {
@@ -346,7 +499,7 @@
}
/**
* 核心功能 3: 路径规划
* 路径规划
*/
function startRouteSearch() {
// 获取输入框的文字

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 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,12 +1,25 @@
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):
@@ -28,15 +41,14 @@ 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
- shared_preferences_foundation (0.0.1):
@@ -47,6 +59,7 @@ 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`)
@@ -57,7 +70,6 @@ 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`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -69,11 +81,19 @@ 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:
@@ -94,8 +114,6 @@ 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"
shared_preferences_foundation:
@@ -105,25 +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_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
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,12 +76,5 @@
</array>
<key>uses</key>
<string></string>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
</array>
</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,4 +1,5 @@
import 'package:encrypt/encrypt.dart';
import 'package:flutter/material.dart' as ui;
class LoginUtil {
static final _keyString = '915eae87951a448c86c47796e44c1fcf';
@@ -26,5 +27,9 @@ class LoginUtil {
final decrypted = _encrypter.decrypt(encrypted);
return decrypted;
}
static ui.Image getAssImg(String imgName){
return ui.Image(image: ui.AssetImage('assets/images/$imgName.png'),fit: ui.BoxFit.cover,);
}
}

View File

@@ -5,7 +5,7 @@ class AppTheme {
static const String Font_YuYang = 'YuYang';
static const Color themeColor = Color(0xFF0c83c3);
static const Color themeColor = Color(0xFF017137);
//是否开放域名切换
static const bool is_show_host = false;

View File

@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get_storage/get_storage.dart';
@@ -8,8 +9,8 @@ import 'package:ln_jq_app/storage_service.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'common/styles/theme.dart';
import 'pages/home/view.dart';
import 'pages/login/view.dart';
import 'pages/welcome/view.dart'; // 引入启动页
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -17,8 +18,10 @@ void main() async {
WidgetsBinding widgetsBinding = await init(
isDebug: false,
logTag: '小羚羚',
supportedLocales: [Locale('zh', 'CN')],
supportedLocales: [const Locale('zh', 'CN')],
);
// 保持原生闪屏页,直到 WelcomeController 调用 remove()
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await GetStorage.init();
@@ -42,22 +45,17 @@ void main() async {
darkTheme: AppTheme.light,
// AppTitle
title: '小羚羚',
// 首页入口
home: HomePage(),
//组件国际化
fallbackLocale: Locale('zh', 'CN'),
supportedLocales: [Locale('zh', 'CN')],
// 将入口改为启动页
home: const WelcomePage(),
fallbackLocale: const Locale('zh', 'CN'),
supportedLocales: const [Locale('zh', 'CN')],
localizationsDelegates: const [
//pull_to_refresh
RefreshLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// Builder
builder: (context, widget) {
// do something....
return widget!;
},
),
@@ -67,11 +65,8 @@ void main() async {
void initHttpSet() {
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
// 设置基础 URL
HttpService.to.setBaseUrl(AppTheme.test_service_url);
//指定请求头
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
// 设置全局响应处理器
HttpService.to.setOnResponseHandler((response) async {
try {
if (response.data == null) {
@@ -79,11 +74,10 @@ void initHttpSet() {
}
final baseModel = BaseModel.fromJson(response.data);
if (baseModel.code == 0 || baseModel.code == 200) {
return null;
} else if (baseModel.code == 401) {
await StorageService.to.clearLoginInfo();
Get.offAll(() => LoginPage());
// Get.offAll(() => const LoginPage());
return baseModel.message;
} else {
return (baseModel.error.toString()).isEmpty

View File

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

View File

@@ -45,6 +45,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
customStartTime = DateTime.now();
customEndTime = customStartTime!.add(const Duration(days: 1));
renderData();
msgNotice(); // 红点消息
startAutoRefresh();
}
@@ -94,6 +95,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
String jobDetailsStr = "";
String jobId = "";
Timer? _refreshTimer;
bool isNotice = false;
Future<void> renderData() async {
showLoading("加载中");
@@ -168,7 +170,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
}
jobDetailsStr =
"当前站点已设置$beginTime至$endTime,共${hoursLeft.toStringAsFixed(2)}小时,为$hydStatusStr状态";
"当前站点已设置$beginTime至$endTime,共${hoursLeft.toStringAsFixed(2)}小时,为$hydStatusStr状态";
// 如果是处于非营运状态,自动回填开始和结束时间
// 假设 customStartTime 是现在customEndTime 是接口返回的结束时间
@@ -210,7 +212,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
var customerPriceTemp = result.data["customerPrice"];
customerPrice =
(customerPriceTemp != null && customerPriceTemp.toString().isNotEmpty)
(customerPriceTemp != null && customerPriceTemp.toString().isNotEmpty)
? "$customerPriceTemp"
: "暂无价格";
@@ -246,6 +248,27 @@ class ReservationController extends GetxController with BaseControllerMixin {
}
}
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
void onOperationStatusChanged(String? newValue) {
if (newValue != null) {
selectedOperationStatus = newValue;

View File

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

View File

@@ -6,6 +6,7 @@ import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/storage_service.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
enum ReservationStatus {
pending, // 待处理 ( addStatus: 0)
@@ -143,6 +144,8 @@ class SiteController extends GetxController with BaseControllerMixin {
Timer? _refreshTimer;
final TextEditingController searchController = TextEditingController();
bool isNotice = false;
final RefreshController refreshController = RefreshController(initialRefresh: false);
@override
bool get listenLifecycleEvent => true;
@@ -167,7 +170,7 @@ class SiteController extends GetxController with BaseControllerMixin {
searchController.dispose();
super.onClose();
}
bool isNotice = false;
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
@@ -199,6 +202,8 @@ class SiteController extends GetxController with BaseControllerMixin {
});
}
void onRefresh() => renderData(isRefresh: true);
///停止定时器的方法
void stopAutoRefresh() {
// 如果定时器存在并且是激活状态,就取消它
@@ -221,7 +226,7 @@ class SiteController extends GetxController with BaseControllerMixin {
'pageNum': 1,
'pageSize': 50, // 暂时不考虑分页一次获取30条
'keyword': searchText, // 加氢站名称、手机号
'stationId': StorageService.to.userId
'stationId': StorageService.to.userId,
},
);
@@ -587,7 +592,7 @@ class SiteController extends GetxController with BaseControllerMixin {
String orderTotalAmount = "";
String orderUnfinishedAmount = "";
Future<void> renderData() async {
Future<void> renderData({bool isRefresh = false}) async {
try {
var responseData = await HttpService.to.get(
'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}',
@@ -620,6 +625,10 @@ class SiteController extends GetxController with BaseControllerMixin {
} finally {
//加载列表数据
fetchReservationData();
if (isRefresh) {
refreshController.refreshCompleted();
}
}
}
}

File diff suppressed because it is too large Load Diff

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

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

View File

@@ -12,12 +12,12 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
@override
Widget build(BuildContext context) {
Get.put(AttachmentViewerController());
final fileName = controller.url.split('/').last;
// final fileName = controller.url.split('/').last;
return Scaffold(
appBar: AppBar(
title: Text(
fileName,
"证件详情",
style: const TextStyle(fontSize: 16),
overflow: TextOverflow.ellipsis,
),

View File

@@ -6,7 +6,7 @@ import 'package:path_provider/path_provider.dart';
import 'attachment_viewer_page.dart';
class CertificateViewerController extends GetxController with BaseControllerMixin{
class CertificateViewerController extends GetxController with BaseControllerMixin {
late final String title;
late final List<String> attachments;
@@ -78,18 +78,11 @@ class CertificateViewerController extends GetxController with BaseControllerMixi
return;
}
Get.to(
() => const AttachmentViewerPage(),
arguments: {
'url': url,
},
);
Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
}
/// 检查 URL 是否为 PDF (此方法保持不变)
bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf');
}
}

View File

@@ -2,10 +2,12 @@ import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/pages/c_page/car_info/attachment_viewer_page.dart';
import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import 'package:path_provider/path_provider.dart';
import 'certificate_viewer_page.dart';
import 'dart:io';
class CarInfoController extends GetxController with BaseControllerMixin {
@override
@@ -22,11 +24,38 @@ class CarInfoController extends GetxController with BaseControllerMixin {
final RxList<String> operationAttachments = <String>[].obs;
final RxList<String> hydrogenationAttachments = <String>[].obs;
final RxList<String> registerAttachments = <String>[].obs;
String color = "";
String hydrogenCapacity = "";
String rentFromCompany = "";
String address = "";
bool isNotice = false;
@override
void onInit() {
super.onInit();
getUserBindCarInfo();
msgNotice();
}
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
@override
@@ -96,6 +125,21 @@ class CarInfoController extends GetxController with BaseControllerMixin {
parseAttachments(data['hydrogenationAttachment']),
);
registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
// 初始化时开始加载所有PDF
attachments = [
...drivingAttachments,
...operationAttachments,
...hydrogenationAttachments,
...registerAttachments,
];
color = data['color'].toString();
hydrogenCapacity = data['hydrogenCapacity'].toString();
rentFromCompany = data['rentFromCompany'].toString();
address = data['address'].toString();
loadAllPdfs();
}
}
updateUi();
@@ -117,4 +161,69 @@ class CarInfoController extends GetxController with BaseControllerMixin {
arguments: {'title': title, 'attachments': attachments},
);
}
/// 导航到通用的附件查看器页面
void openAttachment(String url) {
if (url.isEmpty) {
showErrorToast('附件链接无效');
return;
}
Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
}
/// 检查 URL 是否为 PDF
bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf');
}
List<String> attachments = [];
// --- 新增: 状态管理 ---
/// 用于存储网络PDF的本地路径key是网络urlvalue是本地路径
final RxMap<String, String> localPdfPaths = <String, String>{}.obs;
/// 用于跟踪每个附件的加载状态key是网络url
final RxMap<String, bool> isLoading = <String, bool>{}.obs;
/// 遍历所有附件如果是PDF则进行下载
void loadAllPdfs() {
for (var url in attachments) {
if (isPdf(url)) {
_downloadPdf(url);
}
}
}
/// 下载单个PDF文件
Future<void> _downloadPdf(String url) async {
if (url.isEmpty) return;
// 开始加载
isLoading[url] = true;
try {
final dio = Dio();
final Directory tempDir = await getTemporaryDirectory();
final String savePath = '${tempDir.path}/${url.split('/').last}';
// 检查文件是否已存在,避免重复下载
if (await File(savePath).exists()) {
localPdfPaths[url] = savePath;
isLoading[url] = false;
return;
}
await dio.download(url, savePath);
// 下载成功后,更新本地路径
localPdfPaths[url] = savePath;
} catch (e) {
print('PDF download error for $url: $e');
// 出错时也可以更新状态以便UI显示错误提示
} finally {
// 结束加载
isLoading[url] = false;
}
}
}

View File

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

View File

@@ -35,9 +35,9 @@ class MineController extends GetxController with BaseControllerMixin {
String historyBreakRules = "";
String vin = "";
String plateNumber = "";
String violationTotal = "0";
String violationScore = "0";
String violationDispose = "0";
String violationTotal = "0";//违章总数
String violationScore = "0";//扣分总数
String violationDispose = "0";//已处理
bool isNotice = false;
void renderData() async {

View File

@@ -1,8 +1,9 @@
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/styles/theme.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import 'controller.dart';
@@ -17,25 +18,32 @@ class MinePage extends GetView<MineController> {
id: 'mine',
builder: (_) {
return Scaffold(
backgroundColor: Colors.grey[100],
backgroundColor: const Color.fromRGBO(247, 249, 251, 1),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildUserInfoCard(),
const SizedBox(height: 5),
_buildDriverScoreCard(),
const SizedBox(height: 5),
_buildMonthlyRecordCard(),
const SizedBox(height: 5),
_buildTipsCard(),
const SizedBox(height: 20),
_buildLogoutButton(),
const SizedBox(height: 20),
],
),
child: Column(
children: [
_buildUserInfoCard(),
const SizedBox(height: 8),
// 新 UI 模块开始
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
children: [
_buildWalletCard(),
SizedBox(height: 16.h),
_buildGridMenu(),
SizedBox(height: 16.h),
_buildRecommendCard(context),
SizedBox(height: 8.h),
_buildSafetyReminderCard(),
SizedBox(height: 24.h),
_buildLogoutButton(),
SizedBox(height: 95.h),
],
),
),
// 新 UI 模块结束
],
),
),
);
@@ -43,82 +51,131 @@ class MinePage extends GetView<MineController> {
);
}
/// 1. 构建顶部用户信息卡片
/// 构建顶部用户信息卡片
Widget _buildUserInfoCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 1,
color: Colors.white,
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 40),
// 增加了顶部 padding 适配状态栏
child: Row(
children: [
const CircleAvatar(
radius: 25,
backgroundColor: Colors.blue,
child: Icon(Icons.person, color: Colors.white, size: 40),
Stack(
children: [
CircleAvatar(
radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
),
Positioned(
right: 0,
bottom: 0,
child: SizedBox(
height: 16.h,
width: 16.w,
child: LoginUtil.getAssImg('ic_logo@2x'),
),
),
],
),
const SizedBox(width: 16),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${StorageService.to.name}",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
Row(
children: [
Text(
"${StorageService.to.name}",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8.w),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(236, 255, 234, 1), // 极浅绿色背景
border: Border.all(color: const Color(0xFFB7E19F)), // 边框
borderRadius: BorderRadius.circular(12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)),
// 叶子图标
SizedBox(width: 4),
Text(
"绿色先锋",
style: TextStyle(
color: Color(0xFF52C41A),
fontSize: 10,
),
),
],
),
),
],
),
const SizedBox(height: 4),
Text(
"${StorageService.to.phone}",
style: TextStyle(color: Colors.grey, fontSize: 11),
),
const SizedBox(height: 4),
Text(
StorageService.to.hasVehicleInfo ? "已绑定车辆" : '未绑定车辆',
style: TextStyle(color: Colors.orange, fontSize: 12),
"羚牛ID${StorageService.to.phone}",
style: const TextStyle(color: Colors.grey, fontSize: 11),
),
],
),
),
IconButton(
onPressed: () async {
// 跳转消息中心
onPressed: () async{
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
// 这里的 style 是为了模拟你图片里的灰色圆形背景
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
// label: Text('3'), // 如果你想显示数字,就加 label
smallSize: 8,
// 红点的大小
backgroundColor: controller.isNotice ? Colors.red : Colors.white,
// 红点颜色
child: Icon(
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 25,
size: 30,
),
),
),
],
),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildStatItem(controller.violationTotal, '违章总数'),
_buildStatItem(controller.violationScore, '扣分总数'),
_buildStatItem(controller.violationDispose, '已处理'),
_buildModernStatItem('服务天数', 'service days', '156', ''),
const SizedBox(width: 8),
_buildModernStatItem('准时率', 'Punctuality', controller.rate, ''),
const SizedBox(width: 8),
_buildModernStatItem('司机评分', 'Driver rating', controller.rating, ''),
],
),
),
@@ -127,242 +184,351 @@ class MinePage extends GetView<MineController> {
);
}
// 用户信息卡片中的小统计项
Widget _buildStatItem(String value, String label) {
// 统计项
Widget _buildModernStatItem(String title, String subtitle, String value, String unit) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Text(subtitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.black54)),
],
),
],
),
),
);
}
/// 我的钱包卡片
Widget _buildWalletCard() {
return Card(
elevation: 1,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"我的钱包",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
Text("User wallet", style: TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
Text(
"¥ 0,00元",
style: TextStyle(
color: Colors.green[700],
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
/// 2x2 功能网格菜单
Widget _buildGridMenu() {
return Column(
children: [
Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
Row(
children: [
_buildGridItem(Icons.person_search_outlined, "客服评价", "3项可评"),
const SizedBox(width: 19),
_buildGridItem(
Icons.assignment_late_outlined,
"违章处理",
"${controller.historyBreakRules}项待办",
countColor: Colors.red,
),
],
),
const SizedBox(height: 16),
Row(
children: [
_buildGridItem(Icons.book_outlined, "安全培训", "0个待看"),
const SizedBox(width: 19),
_buildGridItem(
Icons.verified_user_outlined,
"诚信加氢值",
"845",
isSpecial: true,
backgroundColor: const Color(0xFF006633),
),
],
),
],
);
}
/// 2. 构建驾驶得分卡片
Widget _buildDriverScoreCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
Widget _buildGridItem(
IconData icon,
String title,
String subtitle, {
Color? countColor,
bool isSpecial = false,
Color? backgroundColor,
}) {
return Expanded(
child: Container(
height: 100,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSpecial ? backgroundColor : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'驾驶得分',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
Icon(icon, color: isSpecial ? Colors.white : Colors.black87, size: 28),
const SizedBox(height: 8),
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: isSpecial ? Colors.white : Colors.black87,
),
),
const Text('本月表现', style: TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 20),
Center(
child: SizedBox(
width: 100,
height: 100,
child: Stack(
fit: StackFit.expand,
Text(
subtitle,
style: TextStyle(
fontSize: 12,
color: isSpecial
? Colors.white.withOpacity(0.8)
: (countColor ?? Colors.grey),
),
),
],
),
),
);
}
/// 我要推荐卡片
Widget _buildRecommendCard(BuildContext context) {
return Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircularProgressIndicator(
value: (double.tryParse(controller.rating) ?? 0) / 10,
strokeWidth: 8,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
Text(
"我要推荐",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
Center(
child: Text(
controller.rating,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
Text("Recommend", style: TextStyle(fontSize: 12, color: Colors.grey)),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("累计奖励(积分)", style: TextStyle(fontSize: 11, color: Colors.grey)),
Text(
"0,00",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
),
),
const SizedBox(height: 20),
_buildScoreDetailRow(Icons.directions_car, '安全驾驶', '无违章记录', true),
const Divider(),
_buildScoreDetailRow(Icons.timer, '准时率', '100%准时到达', true),
const Divider(),
_buildScoreDetailRow(Icons.thumb_up, '服务质量', '用户满意度高', true),
const Divider(),
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Row(
children: [
const Text(
'优秀驾驶员',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
),
child: const Text(
'A+',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
],
),
),
],
),
),
);
}
// 驾驶得分卡片中的评分项
Widget _buildScoreDetailRow(
IconData icon,
String title,
String subtitle,
bool isCompleted,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 24),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
Text(subtitle, style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
if (isCompleted) const Icon(Icons.check_circle, color: Colors.blue),
],
),
);
}
/// 3. 构建本月记录卡
Widget _buildMonthlyRecordCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'本月记录',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (GetPlatform.isIOS) {
// 跳转到 iOS 应用商店 (这里使用一个通用的应用商店链接模板,请确保替换为正式的 AppID)
openWebPage("https://apps.apple.com/cn/app/羚牛氢能/6756245815");
} else if (GetPlatform.isAndroid) {
// Android 弹出二维码图
_showAndroidDownloadDialog(context);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF006633),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 0,
),
child: const Text(
"下载推荐",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 8),
_buildRecordRow(Icons.rate_review, '加氢预约践行率', controller.rate),
const Divider(),
_buildRecordRow(
Icons.report_problem_outlined,
'违章',
"${controller.historyBreakRules}",
),
const Divider(),
_buildRecordRow(Icons.car_crash_outlined, '交通事故', "${controller.accident}"),
],
),
),
);
}
// 本月记录中的列表项
Widget _buildRecordRow(IconData icon, String title, String value) {
return ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
radius: 20,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 24),
/// Android 端下载二维码弹窗
void _showAndroidDownloadDialog(BuildContext context) {
Get.dialog(
Center(
child: Container(
width: 280.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const Text(
"扫描二维码下载",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 20),
// 使用 LoginUtil.getAssImg 加载你的图片 android_apk_img.png
SizedBox(
width: 180.w,
height: 180.w,
child: LoginUtil.getAssImg('android_apk_img'),
),
const SizedBox(height: 16),
const Text(
"请让被推荐人扫描上方二维码进行下载安装",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: Colors.grey),
),
],
),
),
const Divider(height: 1),
TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(minimumSize: const Size(double.infinity, 50)),
child: const Text(
"确 定",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
],
),
),
),
title: Text(title, style: const TextStyle(fontSize: 14)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(value, style: const TextStyle(color: AppTheme.themeColor, fontSize: 14)),
],
),
onTap: () {
// TODO: 处理点击事件
},
);
}
/// 4. 构建提示信息卡片
Widget _buildTipsCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_buildInfoItem(Icons.info_outline, '保持良好的驾驶习惯,提高安全评分'),
const SizedBox(height: 10),
_buildInfoItem(Icons.rule, '遵守交通规则,避免违章扣分'),
const SizedBox(height: 10),
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
const SizedBox(height: 10),
Row(
children: [
Icon(Icons.verified_outlined, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: FutureBuilder<String>(
future: getVersion(),
builder: (context, snapshot) {
// 判断是否还在加载
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("");
}
// 如果加载完成且有数据
if (snapshot.hasData) {
return TextX.labelSmall(
"当前版本: ${snapshot.data}",
color: Colors.black54,
);
}
// 错误处理
/// 安全提醒卡片
Widget _buildSafetyReminderCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(242, 249, 248, 1), // 极浅绿色背景
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, color: Colors.green[700], size: 24),
const SizedBox(width: 8),
Text(
"安全提醒",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.green[900],
),
),
],
),
const SizedBox(height: 12),
Text(
"请保持良好驾驶习惯,提高安全评分,遵守交通规则,避免违章扣分。",
style: TextStyle(fontSize: 13, color: Colors.green[800], height: 1.5),
),
const SizedBox(height: 8),
Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(fontSize: 13, color: Colors.green[800]),
),
Row(
children: [
Expanded(
child: FutureBuilder<String>(
future: getVersion(),
builder: (context, snapshot) {
// 判断是否还在加载
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("");
},
),
),
],
),
],
),
),
);
}
}
// 提示信息卡片中的列表项
Widget _buildInfoItem(IconData icon, String text) {
return Row(
children: [
Icon(icon, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)),
),
],
// 如果加载完成且有数据
if (snapshot.hasData) {
return TextX.labelSmall(
"当前版本: ${snapshot.data}",
color: Colors.green[800],
);
}
// 错误处理
return const Text("");
},
),
),
],
),
],
),
);
}
@@ -372,7 +538,7 @@ class MinePage extends GetView<MineController> {
controller.logout();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[400],
backgroundColor: Color.fromRGBO(204, 52, 46, 1),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),

View File

@@ -33,6 +33,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'reservation';
C_ReservationController();
final DateTime _now = DateTime.now();
// 计算当前时间属于哪个1小时区间
@@ -342,7 +344,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
return;
}*/
final reservationEndDateTime = DateTime(
DateTime reservationEndDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
@@ -350,7 +352,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
endTime.value.minute,
);
//判断预约区间的结束时间是否早于当前时间留出1分钟缓冲
// 如果结束的小时数小于开始的小时数,或者结束时间是 00:00说明是次日
if (endTime.value.hour < startTime.value.hour ||
(endTime.value.hour == 0 && endTime.value.minute == 0)) {
reservationEndDateTime = reservationEndDateTime.add(const Duration(days: 1));
}
// 执行时间检查
if (reservationEndDateTime.isBefore(
DateTime.now().subtract(const Duration(minutes: 1)),
)) {
@@ -363,10 +371,10 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
(s) => s.hydrogenId == selectedStationId.value,
);
if (selectedStation.siteStatusName != "营运中") {
/*if (selectedStation.siteStatusName != "营运中") {
showToast("该站点${selectedStation.siteStatusName},暂无法预约");
return;
}
}*/
showLoading("提交中");
@@ -536,6 +544,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
String leftHydrogen = "0";
num maxHydrogen = 0;
String difference = "";
var progressValue = 0.0;
//用来管理查看预约的弹窗
Worker? _sheetWorker;
@@ -551,16 +560,35 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
getUserBindCarInfo();
getSiteList();
startAutoRefresh();
msgNotice();
if (!init) {
_setupListener();
init = true;
}
}
@override
void onPaused() {
stopAutoRefresh();
super.onPaused();
bool isNotice = false;
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
void startAutoRefresh() {
@@ -631,8 +659,10 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
var result = BaseModel.fromJson(responseData.data);
fillingWeight =
"${result.data["fillingWeight"]}${result.data["fillingWeightUnit"]}";
final value = double.tryParse(result.data["fillingWeight"]?.toString() ?? '0') ?? 0;
final String formatted = value.toStringAsFixed(2);
fillingWeight = "$formatted${result.data["fillingWeightUnit"]}";
fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}";
updateUi();
@@ -644,9 +674,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
void getCatinfo() async {
try {
HttpService.to.setBaseUrl(AppTheme.car_service_url);
var responseData = await HttpService.to.post(
'VehicleData/getHydrogenInfoByPlateNumber',
'appointment/vehicle/getHydrogenInfoByPlateNumber',
data: {
'userName': "xll@lingniu",
'password': "4q%3!l6s0p",
@@ -671,11 +700,28 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
amountController.text = flooredDifference.toString();
}
if (maxHydrogen > 0) {
progressValue = leftHydrogenNum / maxHydrogen;
// 边界处理:确保值在 0 到 1 之间
if (progressValue > 1.0) progressValue = 1.0;
if (progressValue < 0.0) progressValue = 0.0;
}
updateUi();
} catch (e) {
} finally {
HttpService.to.setBaseUrl(AppTheme.test_service_url);
}
} catch (e) {}
renderSliderTheme();
}
double current = 0.0;
double maxVal = 0.0;
void renderSliderTheme() {
current = double.tryParse(amountController.text) ?? 0.0;
maxVal = double.tryParse(difference) ?? 100.0;
if (maxVal <= 0) maxVal = 100.0;
updateUi();
}
void getSiteList() async {
@@ -735,7 +781,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
if (stationOptions.isEmpty) {
showToast('附近暂无可用加氢站');
} else {
showToast('站点列表已刷新');
// showToast('站点列表已刷新');
}
// 找到第一个可选的站点作为默认值
@@ -783,6 +829,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
@override
void onClose() {
super.onClose();
amountController.dispose();
plateNumberController.dispose();
if (_debounce != null) {
@@ -790,6 +837,5 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
}
_sheetWorker?.dispose();
stopAutoRefresh();
super.onClose();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get.dart';
class WebController extends GetxController {
late String title;
late String url;
final RxDouble progress = 0.0.obs;
InAppWebViewController? webViewController;
@override
void onInit() {
super.onInit();
// 从参数中获取标题和URL
title = Get.arguments['title'] ?? '详情';
url = Get.arguments['url'] ?? '';
}
void onWebViewCreated(InAppWebViewController controller) {
webViewController = controller;
}
void onProgressChanged(InAppWebViewController controller, int progressValue) {
progress.value = progressValue / 100;
}
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get.dart';
import 'controller.dart';
class WebViewPage extends GetView<WebController> {
const WebViewPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Get.put(WebController());
return Scaffold(
appBar: AppBar(
title: Text(controller.title),
centerTitle: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(2.0),
child: Obx(
() => controller.progress.value < 1.0
? LinearProgressIndicator(
value: controller.progress.value,
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).primaryColor,
),
minHeight: 2.0,
)
: const SizedBox(height: 2.0),
),
),
),
body: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(controller.url)),
initialSettings: InAppWebViewSettings(
isInspectable: true,
javaScriptEnabled: true,
javaScriptCanOpenWindowsAutomatically: true,
useShouldOverrideUrlLoading: true,
mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
),
onWebViewCreated: controller.onWebViewCreated,
onProgressChanged: controller.onProgressChanged,
),
);
}
}

View File

@@ -43,7 +43,8 @@ class HomeController extends GetxController with BaseControllerMixin {
}
} else {
// 未登录,直接去登录页
return LoginPage();
return BaseWidgetsPage();
// return LoginPage();
}
}

View File

@@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
class LoginController extends GetxController with BaseControllerMixin {
@override
@@ -7,19 +10,73 @@ class LoginController extends GetxController with BaseControllerMixin {
LoginController();
// 控制输入框的 TextEditingController
final TextEditingController driverIdentityController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TextEditingController codeController = TextEditingController();
final TextEditingController stationIdController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
// --- 倒计时逻辑 ---
final RxInt countdown = 0.obs;
Timer? _timer;
void startCountdown() async {
if (phoneController.text.isEmpty || !phoneController.text.isPhoneNumber) {
showToast("请输入正确的手机号");
return;
}
if (countdown.value > 0) return;
// 调用发送验证码接口
var responseData = await HttpService.to.post(
'appointment/login/sendCode',
data: {"mobile": phoneController.text},
);
if (responseData == null) {
showToast('验证码发送失败,请稍后重试');
return;
}
try {
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
return;
}
showToast("验证码已发送");
countdown.value = 60;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (countdown.value > 0) {
countdown.value--;
} else {
_timer?.cancel();
}
});
} catch (e) {
showToast('验证码服务异常,请稍后重试');
}
}
@override
void onInit() {
super.onInit();
}
@override
void onClose() {
_timer?.cancel();
phoneController.dispose();
codeController.dispose();
stationIdController.dispose();
passwordController.dispose();
super.onClose();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'package:dio/dio.dart' as dio;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
@@ -6,243 +7,156 @@ import 'package:image_picker/image_picker.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/storage_service.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:permission_handler/permission_handler.dart';
class QrCodeController extends GetxController
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
class QrCodeController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'qrcode';
// --- Animation ---
late final AnimationController animationController;
late final Animation<double> scanAnimation;
// --- 使用 MobileScanner 的控制器 ---
final MobileScannerController scannerController = MobileScannerController();
final RxBool isFlashOn = false.obs;
final RxBool isProcessingResult = false.obs;
final RxBool hasPermission = false.obs;
final RxBool hasCameraPermission = false.obs;
@override
void onInit() {
super.onInit();
requestPermission();
animationController = AnimationController(
duration: const Duration(milliseconds: 2500),
vsync: this,
);
scanAnimation =
Tween<double>(begin: 0, end: 1).animate(animationController);
animationController.repeat(reverse: false);
_checkPermission();
}
/// MobileScanner 的 onDetect 回调方法
void onDetect(BarcodeCapture capture) {
if (isProcessingResult.value) return;
final Barcode? barcode = capture.barcodes.firstOrNull;
if (barcode?.rawValue != null && barcode!.rawValue!.isNotEmpty) {
isProcessingResult.value = true;
scannerController.stop();
animationController.stop();
renderResult(barcode.rawValue!);
}
/// 检查权限
void _checkPermission() async {
var status = await Permission.camera.status;
hasCameraPermission.value = status.isGranted;
}
/// 恢复扫描状态
void resumeScanner() {
isProcessingResult.value = false;
try {
scannerController.start();
animationController.repeat(reverse: false);
} catch (e) {
print("无法重启相机: $e");
}
}
/// 从相册选择图片并扫描二维码
void scanFromGallery() async {
try {
final XFile? imageFile =
await ImagePicker().pickImage(source: ImageSource.gallery);
if (imageFile == null) {
return;
}
scannerController.stop();
animationController.stop();
showLoading("正在识别...");
final BarcodeCapture? capture =
await scannerController.analyzeImage(imageFile.path);
dismissLoading();
final Barcode? firstBarcode = capture?.barcodes.firstOrNull;
if (firstBarcode?.rawValue != null &&
firstBarcode!.rawValue!.isNotEmpty) {
final String scanResult = firstBarcode.rawValue!;
print("相册识别到的内容: $scanResult");
renderResult(scanResult);
} else {
showErrorToast('未识别到二维码');
resumeScanner();
}
} catch (e, stackTrace) {
dismissLoading();
showErrorToast('从相册选择失败,请稍后重试');
print("scanFromGallery Error: $e\n$stackTrace");
resumeScanner();
}
}
/// 切换闪光灯
void toggleFlash() async {
try {
await scannerController.toggleTorch();
final currentTorchState = scannerController.value.torchState;
isFlashOn.value = currentTorchState == TorchState.on;
} catch (e) {
print("切换闪光灯失败: $e");
showErrorToast("无法打开闪光灯");
}
}
/// 翻转相机
void flipCamera() async {
await scannerController.switchCamera();
}
/// 请求相机权限
void requestPermission() async {
/// 1. 拍照并识别车牌流程
void takePhotoAndRecognize() async {
var status = await Permission.camera.request();
hasPermission.value = status.isGranted;
if (!status.isGranted) {
if (status.isPermanentlyDenied) {
showErrorToast('相机权限已被永久拒绝,请到系统设置中开启');
// 延迟一会再引导用户去设置
Future.delayed(const Duration(seconds: 2), () => openAppSettings());
} else {
showErrorToast('请授予相机权限以使用扫描功能');
}
if (status.isPermanentlyDenied) openAppSettings();
showErrorToast("需要相机权限才能拍照识别");
return;
}
}
final XFile? photo = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80, // 压缩图片质量以加快上传
);
if (photo == null) return;
void requestPhotoPermission() async {
var status = await Permission.photos.request();
if (status.isGranted) {
scanFromGallery();
} else if (status.isPermanentlyDenied) {
openAppSettings();
} else {
showErrorToast('需要相册权限才能从相册中选择图片');
}
}
/// 处理扫描结果
void renderResult(String resultStr, {plateNumber}) async {
showLoading("正在获取车辆信息...");
try {
final Map<String, dynamic> requestData = {
"code": resultStr,
"phone": StorageService.to.phone,
};
if (plateNumber != null && plateNumber.isNotEmpty) {
requestData['plateNumber'] = plateNumber;
}
var responseData = await HttpService.to.post(
"appointment/truck/bindTruck",
data: requestData,
);
if (responseData == null) {
dismissLoading();
showToast('无法获取车辆信息,请检查网络或稍后重试');
resumeScanner();
return;
}
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
resumeScanner(); // 绑定失败也要恢复扫描
return;
}
if (result.data == null) {
dismissLoading();
showBindDialog(resultStr);
return;
}
final vehicle = VehicleInfo.fromJson(result.data as Map<String, dynamic>);
await StorageService.to.saveVehicleInfo(vehicle);
dismissLoading();
Get.back(result: true);
} on DioException catch (_) {
showErrorToast("网络请求失败,请稍后重试");
resumeScanner();
} catch (e, _) {
showErrorToast("处理失败,请稍后重舍");
resumeScanner();
} finally {
if (Get.isDialogOpen ?? false) {
dismissLoading();
// 1.1 上传文件
String? imageUrl = await uploadFile(photo.path);
if (imageUrl != null) {
// 1.2 获取车牌号
String? plateNumber = await getPlateNumber(imageUrl);
if (plateNumber != null) {
// 1.3 弹窗确认
manualInputBind(plateNumber, 0);
}
}
}
/// 显示绑定确认对话框
void showBindDialog(String resultStr) {
final TextEditingController plateNumberController = TextEditingController();
// 使用 showConfirmDialog它有 onCancel 回调
/// 手动输入车牌绑定
void manualInputBind(String plateNumber, int source) {
final TextEditingController controller = TextEditingController(
text: plateNumber.toUpperCase() ?? '',
);
DialogX.to.showConfirmDialog(
title: '输入车牌',
barrierDismissible: false,
content: TextField(
controller: plateNumberController,
autofocus: false,
decoration: const InputDecoration(
hintText: '请输入完整的车牌号',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12.0),
title: source == 0 ? "识别结果" : '手动输入车牌',
content: SizedBox(
height: 40.h,
child: TextField(
textAlign: TextAlign.start,
controller: controller,
autofocus: plateNumber.isEmpty,
textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration(
contentPadding: EdgeInsets.only(left: 5),
hintText: '请输入完整的车牌号',
border: OutlineInputBorder(),
),
),
),
confirmText: '确认绑定',
cancelText: '取消', // showConfirmDialog 有 cancelText
onConfirm: () {
final String plateNumber = plateNumberController.text.trim();
if (plateNumber.isEmpty) {
showToast("请输入车牌号");
// 返回 false 可以阻止弹窗关闭,让用户继续输入
return false;
}
renderResult(resultStr, plateNumber: plateNumber);
//关闭弹窗
return true;
},
cancelText: source == 0 ? "重新拍摄" : '取消',
onCancel: () {
// 如果用户点击取消,恢复扫描
resumeScanner();
if (source == 0) {
takePhotoAndRecognize();
}
},
onConfirm: () {
final plate = controller.text.trim().toUpperCase();
if (plate.isNotEmpty) {
bindVehicleByPlate(plate);
}
},
);
}
@override
void onClose() {
scannerController.dispose();
animationController.dispose();
super.onClose();
/// 上传图片
Future<String?> uploadFile(String filePath) async {
showLoading("正在上传图片...");
try {
dio.FormData formData = dio.FormData.fromMap({
'file': await dio.MultipartFile.fromFile(filePath, filename: 'ocr_plate.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;
}
/// OCR 识别
Future<String?> getPlateNumber(String imageUrl) async {
showLoading("正在识别车牌...");
try {
var response = await HttpService.to.get(
"appointment/ocr/getPlateNumber",
params: {'imageUrl': imageUrl},
);
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 bindVehicleByPlate(String plateNumber) async {
showLoading("正在绑定车辆...");
try {
var responseData = await HttpService.to.post(
"appointment/truck/bindOcrTruck",
data: {"plateNumber": plateNumber, "phone": StorageService.to.phone},
);
var result = BaseModel.fromJson(responseData?.data);
if (result.code == 0 && result.data != null) {
await StorageService.to.saveVehicleInfo(VehicleInfo.fromJson(result.data));
dismissLoading();
showSuccessToast("绑定成功");
Get.back(result: true);
} else {
showErrorToast(result.error);
}
} catch (e) {
} finally {
dismissLoading();
}
}
}

View File

@@ -1,285 +1,82 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'controller.dart';
class QrCodePage extends GetView<QrCodeController> {
const QrCodePage({super.key});
const QrCodePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Get.put(QrCodeController());
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: const Text('扫码', style: TextStyle(color: Colors.white)),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Get.back(),
),
actions: [
TextButton(
onPressed: controller.requestPhotoPermission,
child: const Text('相册', style: TextStyle(color: Colors.white, fontSize: 16)),
return GetBuilder<QrCodeController>(
init: QrCodeController(),
id: 'qrcode',
builder: (controller) {
return Scaffold(
appBar: AppBar(
title: const Text('绑定车辆'),
centerTitle: true,
elevation: 0,
),
],
),
body: Obx(() { // 1. 使用 Obx 包裹整个 body
// 根据权限状态来决定显示什么
if (controller.hasPermission.value) {
// 如果有权限,显示扫描器
return _buildScannerView(context);
} else {
// 如果没有权限,显示引导界面
return _buildPermissionDeniedView();
}
}),
);
}
Widget _buildScannerView(BuildContext context){
if (!controller.animationController.isAnimating) {
controller.animationController.repeat(reverse: false);
}
return Stack(
alignment: Alignment.center,
children: [
// 使用 MobileScanner 作为扫描视图
MobileScanner(
controller: controller.scannerController,
onDetect: controller.onDetect,
// 您可以自定义扫描框的样式
scanWindow: Rect.fromCenter(
center: Offset(
MediaQuery.of(context).size.width / 2,
MediaQuery.of(context).size.height / 2 - 50,
),
width: 250,
height: 250,
),
),
// 扫描动画和覆盖层
_buildScannerOverlay(context),
// 底部的功能按钮
Positioned(bottom: 80, left: 0, right: 0, child: _buildActionButtons()),
],
);
}
body: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: SingleChildScrollView(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 24),
Icon(
Icons.directions_car_rounded,
size: 100,
color: Theme.of(context).primaryColor.withOpacity(0.1),
),
const SizedBox(height: 24),
const Text(
"请选择绑定方式",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
"您可以拍摄照片自动识别,\n或手动输入车牌号进行绑定。",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 60),
Widget _buildPermissionDeniedView() {
// 确保动画是停止的
if (controller.animationController.isAnimating) {
controller.animationController.stop();
}
return Container(
color: Colors.black,
alignment: Alignment.center,
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.no_photography, color: Colors.white70, size: 64),
const SizedBox(height: 16),
const Text(
'需要相机权限',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'请授予相机权限以使用扫码功能。',
style: TextStyle(color: Colors.white70, fontSize: 14),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: controller.requestPermission, // 点击按钮重新请求权限
child: const Text('授予权限'),
),
],
),
);
}
/// 构建扫描区域的覆盖层和动画
Widget _buildScannerOverlay(BuildContext context) {
// 模拟扫描框的位置和大小
const double scanAreaSize = 250.0;
return Stack(
children: [
// 半透明的覆盖层
ColorFiltered(
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.5), BlendMode.srcOut),
child: Stack(
children: [
Container(decoration: const BoxDecoration(color: Colors.transparent)),
Align(
alignment: Alignment.center,
child: Container(
margin: const EdgeInsets.only(bottom: 100), // 微调位置
width: scanAreaSize,
height: scanAreaSize,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12),
// 拍照识别按钮
ElevatedButton.icon(
onPressed: controller.takePhotoAndRecognize,
icon: const Icon(Icons.camera_alt_rounded),
label: const Text("拍照识别车牌"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
],
),
),
// 扫描动画
Align(
alignment: Alignment.center,
child: Container(
margin: const EdgeInsets.only(bottom: 100),
width: scanAreaSize,
height: scanAreaSize,
child: AnimatedBuilder(
animation: controller.scanAnimation,
builder: (context, child) {
return CustomPaint(
painter: ScannerAnimationPainter(
controller.scanAnimation.value,
AppTheme.themeColor,
),
);
},
),
),
),
],
);
}
const SizedBox(height: 20),
/// 构建底部的功能按钮(闪光灯、相册)
Widget _buildActionButtons() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'将二维码/条形码放入框内,即可自动扫描',
style: TextStyle(color: Colors.white, fontSize: 14),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// 闪光灯按钮
_buildIconButton(
onPressed: controller.toggleFlash,
//闪光灯状态的变化
child: Obx(
() => IconButton(
icon: Icon(
controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off,
color: Colors.white,
size: 28,
),
onPressed: controller.toggleFlash,
// 3. 手动输入按钮
OutlinedButton.icon(
onPressed: (){
controller.manualInputBind("",1);
},
icon: const Icon(Icons.edit_note_rounded),
label: const Text("手动输入车牌"),
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
side: BorderSide(color: Theme.of(context).primaryColor, width: 1.5),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
// 翻转相机按钮
_buildIconButton(
onPressed: controller.flipCamera,
child: const Icon(
Icons.flip_camera_ios,
color: Colors.white,
size: 28,
),
),
],
const SizedBox(height: 100), // 底部留白
],
),),
),
],
);
}
Widget _buildIconButton({required VoidCallback onPressed, required Widget child}) {
return Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
shape: BoxShape.circle,
),
child: IconButton(
onPressed: onPressed,
icon: child,
iconSize: 32, // 增大点击区域
),
);
},
);
}
}
/// 扫描动画的绘制器
class ScannerAnimationPainter extends CustomPainter {
final double value;
final Color borderColor;
ScannerAnimationPainter(this.value, this.borderColor);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = borderColor
..strokeWidth = 3
..style = PaintingStyle.stroke;
final cornerLength = 20.0;
// 绘制四个角的边框
// Top-left
canvas.drawPath(
Path()
..moveTo(0, cornerLength)
..lineTo(0, 0)
..lineTo(cornerLength, 0),
paint,
);
// Top-right
canvas.drawPath(
Path()
..moveTo(size.width - cornerLength, 0)
..lineTo(size.width, 0)
..lineTo(size.width, cornerLength),
paint,
);
// Bottom-left
canvas.drawPath(
Path()
..moveTo(0, size.height - cornerLength)
..lineTo(0, size.height)
..lineTo(cornerLength, size.height),
paint,
);
// Bottom-right
canvas.drawPath(
Path()
..moveTo(size.width - cornerLength, size.height)
..lineTo(size.width, size.height)
..lineTo(size.width, size.height - cornerLength),
paint,
);
// 绘制扫描线
final linePaint = Paint()
..color = borderColor.withOpacity(0.8)
..strokeWidth = 2
..shader = LinearGradient(
colors: [borderColor.withOpacity(0), borderColor, borderColor.withOpacity(0)],
stops: const [0.0, 0.5, 1.0],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
final y = size.height * value;
canvas.drawLine(Offset(0, y), Offset(size.width, y), linePaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

View File

@@ -10,12 +10,14 @@ class UrlHostController extends GetxController {
// 预设的域名列表
final List<String> presetUrls = [
'https://beta-esg.api.lnh2e.com/', // 测试环境
'http://47.101.201.13:8443/api/', // 测试环境
'http://192.168.110.44:8080/', // 沈辰本地
'http://192.168.110.222:8080/', // 何斐本地
];
final List<String> urlNames = [
'测试环境',
'线上环境',
'沈辰本地环境',
'何斐本地环境',
];

View File

@@ -0,0 +1,32 @@
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/pages/home/view.dart';
import 'package:ln_jq_app/pages/login/view.dart';
import 'package:ln_jq_app/storage_service.dart';
class WelcomeController extends GetxController {
@override
void onReady() {
super.onReady();
// 移除原生闪屏页(如果有的话)
FlutterNativeSplash.remove();
_startTimer();
}
void _startTimer() {
// 1.5秒后执行跳转逻辑
Future.delayed(const Duration(milliseconds: 1500), () {
Get.offAll(() => const HomePage());
});
}
void _jumpToNextPage() {
if (StorageService.to.isLoggedIn) {
// 已登录,跳转到首页
Get.offAll(() => const HomePage());
} else {
// 未登录,跳转到登录页
Get.offAll(() => const LoginPage());
}
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'controller.dart';
class WelcomePage extends GetView<WelcomeController> {
const WelcomePage({super.key});
@override
Widget build(BuildContext context) {
// 初始化控制器
Get.put(WelcomeController());
return Scaffold(
backgroundColor: Colors.white,
body: SizedBox.expand(
child: Stack(
children: [
Positioned(
top: 0,
bottom: 0,
left: 0,
right: 0,
child: Image.asset(
'assets/images/welcome.png',
fit: BoxFit.fill
),
),
],
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 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.2+5
version: 1.2.3+6
environment:
sdk: ^3.9.0