18 Commits
map_dev ... dev

Author SHA1 Message Date
84b174c4a5 协议通知后注册推送 2026-03-13 17:09:30 +08:00
02e1946319 文字边界 2026-03-05 11:20:14 +08:00
fe44848529 号码 2026-03-04 14:50:21 +08:00
572c416827 枪号回填,v1.2.4 2026-03-04 13:38:26 +08:00
8df16f0787 pdf查看 2026-03-03 17:51:06 +08:00
ce6bd3edd2 bugfix 2026-03-03 13:09:10 +08:00
6997b4ac9e 调整权限和库 2026-03-02 11:28:44 +08:00
a26d2478f3 样式 2026-02-28 17:31:28 +08:00
0dded3b928 ocr识别,历史新增类型 2026-02-28 17:15:42 +08:00
b7caf58adf 加氢站-查看证件 2026-02-28 15:00:56 +08:00
0df1902df2 无预约 2026-02-28 11:59:07 +08:00
a8314d8a7a 关闭弹窗 2026-02-27 10:55:55 +08:00
39cae906e9 加氢站-预约 2026-02-27 10:54:55 +08:00
14fd6c75d0 fix 2026-02-25 15:35:33 +08:00
1724852a39 司机-可上传证件 2026-02-25 15:35:02 +08:00
a05d4ebb9b 调整 2026-02-12 18:01:56 +08:00
600cea4379 进度条 2026-02-12 17:34:06 +08:00
3dfc1dfc2c 样式调整 2026-02-12 16:54:50 +08:00
24 changed files with 1616 additions and 458 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 = 6
versionName = "1.2.3"
versionCode = 7
versionName = "1.2.4"
}
signingConfigs {

View File

@@ -1,35 +1,39 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!--定位权限-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:label="小羚羚"
android:requestLegacyExternalStorage="true"
android:name="${applicationName}"
android:icon="@mipmap/logo">
android:icon="@mipmap/logo"
android:label="小羚羚">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
@@ -37,11 +41,10 @@
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
@@ -51,31 +54,54 @@
android:value="2" />
<!-- 请填写你自己的- appKey -->
<meta-data android:name="com.alibaba.app.appkey" android:value="335642645"/>
<meta-data
android:name="com.alibaba.app.appkey"
android:value="335642645" />
<!-- 请填写你自己的appSecret -->
<meta-data android:name="com.alibaba.app.appsecret" android:value="39628204345a4240b5b645b68a5896c7"/>
<meta-data
android:name="com.alibaba.app.appsecret"
android:value="39628204345a4240b5b645b68a5896c7" />
<!-- 华为通道的参数appid -->
<meta-data android:name="com.huawei.hms.client.appid" android:value="" />
<meta-data
android:name="com.huawei.hms.client.appid"
android:value="" />
<!-- vivo通道的参数api_key为appkey -->
<meta-data android:name="com.vivo.push.api_key" android:value="" />
<meta-data android:name="com.vivo.push.app_id" android:value="" />
<meta-data
android:name="com.vivo.push.api_key"
android:value="" />
<meta-data
android:name="com.vivo.push.app_id"
android:value="" />
<!-- honor通道的参数-->
<meta-data android:name="com.hihonor.push.app_id" android:value="" />
<meta-data
android:name="com.hihonor.push.app_id"
android:value="" />
<!-- oppo -->
<meta-data android:name="com.oppo.push.key" android:value="" />
<meta-data android:name="com.oppo.push.secret" android:value="" />
<meta-data
android:name="com.oppo.push.key"
android:value="" />
<meta-data
android:name="com.oppo.push.secret"
android:value="" />
<!-- 小米-->
<meta-data android:name="com.xiaomi.push.id" android:value="id=2222222222222222222" />
<meta-data android:name="com.xiaomi.push.key" android:value="id=5555555555555" />
<meta-data
android:name="com.xiaomi.push.id"
android:value="id=2222222222222222222" />
<meta-data
android:name="com.xiaomi.push.key"
android:value="id=5555555555555" />
<!-- 魅族-->
<meta-data android:name="com.meizu.push.id" android:value="" />
<meta-data android:name="com.meizu.push.key" android:value="" />
<meta-data
android:name="com.meizu.push.id"
android:value="" />
<meta-data
android:name="com.meizu.push.key"
android:value="" />
<!-- 接收推送消息 -->
@@ -99,6 +125,7 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -117,8 +144,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>

View File

@@ -195,10 +195,10 @@
<div id="search-box">
<div class="input-row">
<input id="startInput" placeholder="起点: 请输入当前地点" />
<input id="startInput" placeholder="起点: 请输入当前地点" onfocus="this.select()" />
</div>
<div class="input-row">
<input id="endInput" placeholder="终点: 请输入目的地" />
<input id="endInput" placeholder="终点: 请输入目的地" onfocus="this.select()" />
<button onclick="startRouteSearch()">路径规划</button>
</div>
</div>
@@ -244,6 +244,11 @@
}
});
// 点击地图空白处重置状态
map.on('click', function() {
resetSearchState();
});
// 添加基础控件
map.addControl(new AMap.Scale());
map.addControl(new AMap.ToolBar({
@@ -285,6 +290,19 @@
});
}
/**
* 重置搜索状态,隐藏面板和路线
*/
function resetSearchState() {
if (document.body.classList.contains('panel-active')) {
console.log("JS->: 重置地图状态");
document.body.classList.remove('panel-active');
var panel = document.getElementById('panel');
panel.style.display = 'none';
if (driving) driving.clear();
}
}
/**
* 核心功能 1: 接收 Flutter 传来的定位数据
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
@@ -481,19 +499,25 @@
offset: new AMap.Pixel(-16, -32),
title: station.name,
label: {
content: '<div class="custom-bubble">' + station.name + '</div>',
content: '<div class="custom-bubble">' + station.name +
'</div>',
direction: 'top'
}
});
// 3. 绑定点击事件:选中即为目的地,并开始规划
sMarker.on('click', function() {
document.getElementById('endInput').value = station.address || station.name;
// 更新当前的 destMarker (如果需要)
if (destMarker) destMarker.setMap(null);
sMarker.on('click', function () {
var stationName = station.name || "目的地";
document.getElementById('endInput').value = station.address ||
stationName;
// 更新当前的 destMarker
if (destMarker && destMarker !== sMarker) destMarker.setMap(null);
destMarker = sMarker;
startRouteSearch();
// 直接传入坐标对象,避免关键字搜索失败
var loc = new AMap.LngLat(station.longitude, station.latitude);
startRouteSearch(loc);
});
stationMarkers.push(sMarker);
@@ -554,11 +578,12 @@
}
}
/**
* 路径规划
* @param {AMap.LngLat} [destLoc] 可选的终点坐标
*/
function startRouteSearch() {
// 获取输入框的文字
function startRouteSearch(destLoc) {
var startKw = document.getElementById('startInput').value;
var endKw = document.getElementById('endInput').value;
@@ -566,28 +591,21 @@
alert("请输入起点");
return;
}
if (!endKw) {
alert("请输入终点");
return;
}
// 清除旧路线
if (driving) driving.clear();
// 收起键盘
document.getElementById('startInput').blur();
document.getElementById('endInput').blur();
// --- 构造路径规划的点 ---
var points = [];
// 1. 处理起点逻辑
// 1. 起点逻辑
if (!startKw || startKw === '我的位置' || startKw.includes('当前位置')) {
if (!currentLng || !currentLat) {
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler('requestLocation');
}
if (window.flutter_inappwebview) window.flutter_inappwebview.callHandler('requestLocation');
alert("正在获取定位,请稍后...");
return;
}
@@ -601,10 +619,17 @@
});
}
// 2. 处理终点逻辑
// 2. 终点逻辑:如果有传入坐标,则直接使用坐标导航,成功率最高
if (destLoc) {
points.push({
keyword: endKw,
location: destLoc // 关键:使用精确坐标
});
} else {
points.push({
keyword: endKw
});
}
// 3. 发起搜索
driving.search(points, function (status, result) {
@@ -613,10 +638,12 @@
var panel = document.getElementById('panel');
panel.style.display = 'block';
document.body.classList.add('panel-active');
} else {
console.log('JS: 规划失败', result);
alert("规划失败,请检查起终点名称");
}
// else {
// console.error('JS: 规划失败', result);
// // 如果坐标规划都失败了,通常是由于起终点距离过近或政策限制(如货车禁行)
// alert("路径规划未成功,请尝试微调起终点");
// }
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 B

After

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -41,6 +41,8 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- saver_gallery (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -62,6 +64,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -103,6 +106,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
saver_gallery:
:path: ".symlinks/plugins/saver_gallery/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
@@ -127,6 +132,7 @@ SPEC CHECKSUMS:
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b

View File

@@ -76,5 +76,11 @@
<string>en</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<!-- 允许在“文件”App中直接打开文档 -->
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
</dict>
</plist>

View File

@@ -1,7 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:intl/intl.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
@@ -25,12 +22,14 @@ class HistoryController extends GetxController with BaseControllerMixin {
final RxBool hasData = false.obs;
String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value);
String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value);
String stationName = "";
final Map<String, String> statusOptions = {
'': '全部',
'100': '未预约加氢',
'0': '待加氢',
'1': '已加氢',
'2': '未加氢',

View File

@@ -78,12 +78,16 @@ class ReservationPage extends GetView<ReservationController> {
children: [
Row(
children: [
Text(
Flexible(
child: Text(
controller.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
_buildStatusTag(),
@@ -98,12 +102,11 @@ class ReservationPage extends GetView<ReservationController> {
),
),
IconButton(
onPressed: () async{
onPressed: () async {
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
@@ -111,9 +114,7 @@ class ReservationPage extends GetView<ReservationController> {
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
@@ -232,14 +233,19 @@ class ReservationPage extends GetView<ReservationController> {
label,
style: TextStyle(color: Colors.grey, fontSize: 11.sp),
),
Text(
Expanded(
child: Text(
value,
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: Color(0xFF333333),
color: const Color(0xFF333333),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
@@ -515,14 +521,15 @@ class ReservationPage extends GetView<ReservationController> {
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text("发送广播", style: TextStyle(color: Colors.white)),
),
),
],
),
],
),
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/history/view.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
@@ -57,17 +56,33 @@ class SitePage extends GetView<SiteController> {
),
GestureDetector(
onTap: () {
Get.to(
() => HistoryPage(),
arguments: {'stationName': controller.name},
);
// 手动录入
controller.confirmReservation("", isAdd: true);
},
child: Text(
'历史记录',
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFEEEEEE)),
),
child: Row(
children: [
const Icon(
Icons.add_circle_outline,
size: 18,
color: Color(0xFF666666),
),
const SizedBox(width: 4),
Text(
"无预约车辆加氢",
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: Color.fromRGBO(156, 163, 175, 1),
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 13.sp,
fontWeight: FontWeight.w400
),
),
],
),
),
),
@@ -185,27 +200,7 @@ class SitePage extends GetView<SiteController> {
],
),
),
IconButton(
onPressed: () async {
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
_buildDropdownMenu(),
],
),
const SizedBox(height: 25),
@@ -233,6 +228,40 @@ class SitePage extends GetView<SiteController> {
);
}
Widget _buildDropdownMenu() {
return PopupMenuButton<String>(
icon: Container(child: const Icon(Icons.grid_view_rounded, size: 24)),
onSelected: (value) async {
if (value == 'message') {
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
} else if (value == 'history') {
Get.to(() => const HistoryPage(), arguments: {'stationName': controller.name});
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'message',
child: Row(
children: [
Icon(Icons.notifications_none, size: 20),
SizedBox(width: 8),
Text('消息中心'),
],
),
),
const PopupMenuItem(
value: 'history',
child: Row(
children: [Icon(Icons.history, size: 20), SizedBox(width: 8), Text('加氢历史')],
),
),
],
);
}
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
return Expanded(
child: Container(
@@ -466,7 +495,7 @@ class SitePage extends GetView<SiteController> {
/// 右侧具体数据卡片
Widget _buildInfoCard(ReservationModel item) {
return Container(
padding: EdgeInsets.only(left: 16.w, top: 8.5, bottom: 8.5, right: 16.w),
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
@@ -508,22 +537,42 @@ class SitePage extends GetView<SiteController> {
),
const SizedBox(height: 8),
// 联系信息
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"${item.contactPerson} | ${item.contactPhone}",
item.contactPerson.isEmpty || item.contactPhone.isEmpty
? ""
: "${item.contactPerson} | ${item.contactPhone}",
style: TextStyle(
color: Color(0xFF999999),
fontSize: 13.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 6.h),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (item.hasDrivingAttachment)
controller.buildInfoTag('行驶证',item.drivingAttachments),
if (item.hasHydrogenationAttachment) ...[
SizedBox(width: 8.w),
controller.buildInfoTag('加氢证',item.hydrogenationAttachments)
],
Spacer(),
if (item.isEdit == "1") ...[
const SizedBox(height: 15),
const Divider(height: 1, color: Color(0xFFF5F5F5)),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: _buildSmallButton(
"修改信息",
isOutline: true,
onTap: () {
controller.confirmReservation(item.id, isEdit: true);
},
),
//操作按钮(仅在待处理状态显示)
if (item.status == ReservationStatus.pending) ...[
),
] else if (item.status == ReservationStatus.pending) ...[
const SizedBox(height: 15),
const Divider(height: 1, color: Color(0xFFF5F5F5)),
const SizedBox(height: 12),
@@ -550,17 +599,20 @@ class SitePage extends GetView<SiteController> {
],
],
),
],
),
);
}
/// 通用小按钮
Widget _buildSmallButton(
String text, {
required bool isOutline,
required VoidCallback onTap,
}) {
const kPrimaryGreen = Color(0xFF006D35);
const kDangerRed = Color(0xFFFF7D7D);
var kDangerRed = text.contains('修改') ? Colors.red : Color.fromRGBO(255, 142, 98, 1);
return GestureDetector(
onTap: onTap,
@@ -634,139 +686,4 @@ class SitePage extends GetView<SiteController> {
),
);
}
/// 右侧操作按钮(拒绝/确认)
Widget _buildActionButtons(ReservationModel item) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// 拒绝按钮(空心)
GestureDetector(
onTap: () => controller.rejectReservation(item.id),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFF7D7D)),
),
child: const Text(
"拒绝",
style: TextStyle(
color: Color(0xFFFF7D7D),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 8),
// 确认按钮(实心深绿)
GestureDetector(
onTap: () => controller.confirmReservation(item.id),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF006D35),
borderRadius: BorderRadius.circular(10),
),
child: const Text(
"确认",
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}
/// 构建状态标签
Widget _buildStatusChip(ReservationStatus status) {
String text;
Color color;
switch (status) {
case ReservationStatus.pending:
text = '待加氢';
color = Colors.orange;
break;
case ReservationStatus.completed:
text = '已加氢';
color = Colors.greenAccent;
break;
case ReservationStatus.rejected:
text = '拒绝加氢';
color = Colors.red;
break;
case ReservationStatus.unadded:
text = '未加氢';
color = Colors.red;
break;
case ReservationStatus.cancel:
text = '已取消';
color = Colors.red;
break;
default:
text = '未知状态';
color = Colors.grey;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, color: color, size: 8),
const SizedBox(width: 4),
Text(
text,
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
),
],
),
);
}
/// 构建信息详情行
Widget _buildDetailRow(
IconData icon,
String label,
String value, {
Color valueColor = Colors.black87,
}) {
return Row(
children: [
Icon(icon, color: Colors.grey, size: 20),
const SizedBox(width: 8),
Text('$label: ', style: const TextStyle(fontSize: 14, color: Colors.grey)),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 14,
color: valueColor,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
/// 底部构建带图标的提示信息行
Widget _buildInfoItem(IconData icon, String text) {
return Row(
children: [
Icon(icon, color: Colors.blue, size: 20),
const SizedBox(width: 8),
Text(text, style: const TextStyle(fontSize: 14, color: Colors.black54)),
],
);
}
}

View File

@@ -41,7 +41,7 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
Widget _buildNavigationBar() {
return SafeArea(
child: Container(
height: 50.h,
height: Get.height * 0.05,
margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
decoration: BoxDecoration(
color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景

View File

@@ -1,5 +1,8 @@
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:getx_scaffold/getx_scaffold.dart' as dio;
import 'package:image_picker/image_picker.dart';
import 'package:image_picker_platform_interface/src/types/image_source.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/pages/c_page/car_info/attachment_viewer_page.dart';
@@ -89,6 +92,113 @@ class CarInfoController extends GetxController with BaseControllerMixin {
}
}
//上传证件照
void pickImage(String title, ImageSource source) async {
if (source == ImageSource.camera) {
takePhotoAndRecognize(title);
} else if (source == ImageSource.gallery) {
//相册选择逻辑
var status = await Permission.photos.request();
if (!status.isGranted) {
if (status.isPermanentlyDenied) openAppSettings();
showErrorToast("需要相册权限才能拍照上传");
return;
}
final XFile? image = await ImagePicker().pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
if (image != null) {
_uploadAndSaveCertificate(title, image.path);
}
}
}
void takePhotoAndRecognize(String title) async {
var status = await Permission.camera.request();
if (!status.isGranted) {
if (status.isPermanentlyDenied) openAppSettings();
showErrorToast("需要相机权限才能拍照上传");
return;
}
final XFile? photo = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80, // 压缩图片质量以加快上传
);
if (photo == null) return;
_uploadAndSaveCertificate(title, photo.path);
}
/// 提取共用的上传与关联证件逻辑
void _uploadAndSaveCertificate(String title, String filePath) async {
// 上传文件
String? imageUrl = await uploadFile(filePath);
if (imageUrl == null) return;
// 根据标题映射业务类型
String type = "";
switch (title) {
case "行驶证":
type = "9";
break;
case "营运证":
type = "10";
break;
case "加氢证":
type = "12";
break;
case "登记证":
type = "13";
break;
default:
return;
}
// 调用后台接口关联证件
var response = await HttpService.to.post(
"appointment/truck/uploadCertificatePic",
data: {
"plateNumber": plateNumber,
"imageUrl": imageUrl,
"type": type,
},
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0) {
showSuccessToast("上传成功");
getUserBindCarInfo(); // 重新拉取数据更新UI
} else {
showErrorToast(result.error);
}
}
}
/// 上传图片
Future<String?> uploadFile(String filePath) async {
showLoading("正在上传...");
try {
dio.FormData formData = dio.FormData.fromMap({
'file': await dio.MultipartFile.fromFile(filePath, filename: 'ocr_identity.jpg'),
});
var response = await HttpService.to.post("appointment/ocr/upload", data: formData);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0) return result.data.toString();
showErrorToast(result.error);
}
} catch (e) {
showErrorToast("图片上传失败");
} finally {
dismissLoading();
}
return null;
}
void getUserBindCarInfo() async {
if (StorageService.to.hasVehicleInfo) {
VehicleInfo? bean = StorageService.to.vehicleInfo;
@@ -102,7 +212,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// 获取证件信息
final response = await HttpService.to.get(
'appointment/vehicle/getPicInfoByVin?vin=$vin',
'appointment/vehicle/getPicInfoByVin?vin=$vin&plateNumber=$plateNumber',
);
if (response != null && response.data != null) {

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:image_picker/image_picker.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/storage_service.dart';
@@ -368,7 +369,7 @@ class CarInfoPage extends GetView<CarInfoController> {
children: [
_buildCertificateContent('行驶证', controller.drivingAttachments),
_buildCertificateContent('营运证', controller.operationAttachments),
_buildCertificateContent('加氢资格', controller.hydrogenationAttachments),
_buildCertificateContent('加氢证', controller.hydrogenationAttachments),
_buildCertificateContent('登记证', controller.registerAttachments),
],
),
@@ -388,7 +389,7 @@ class CarInfoPage extends GetView<CarInfoController> {
child: Padding(
padding: EdgeInsets.all(16.0),
child: attachments.isEmpty
? const Center(child: Text('暂无相关证件信息'))
? _buildEmptyCertificateState(title)
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
@@ -440,6 +441,158 @@ class CarInfoPage extends GetView<CarInfoController> {
});
}
Widget _buildEmptyCertificateState(String title) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/ic_attention@2x.png', // 请替换为您的实际图片路径
width: 120,
height: 120,
),
const SizedBox(height: 16),
Text(
'您未上传“$title',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Color.fromRGBO(51, 51, 51, 1),
),
),
const SizedBox(height: 8),
Text(
'上传后可提前通知加氢站报备\n大幅减少加氢站等待时间',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.sp,
color: Color.fromRGBO(156, 163, 175, 1),
height: 1.5,
),
),
const SizedBox(height: 24),
SizedBox(
width: 200,
height: 44,
child: ElevatedButton.icon(
onPressed: () {
_showUploadDialog(title);
},
icon: const Text(
'立即上传',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
label: Image.asset(
'assets/images/ic_upload@2x.png',
height: 20.h,
width: 20.w,
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF017137),
foregroundColor: Colors.white,
shape: StadiumBorder(),
elevation: 0,
),
),
),
],
);
}
void _showUploadDialog(String title) {
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'上传$title',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 12),
Text(
'请确保拍摄证件清晰可见,关键文字无反光遮挡,这将有助于快速通过审核',
style: TextStyle(fontSize: 13, color: Colors.grey[400], height: 1.5),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: _buildUploadOption(
icon: Icons.camera_alt_outlined,
label: '拍照上传',
onTap: () {
controller.pickImage(title, ImageSource.camera);
Get.back();
},
),
),
const SizedBox(width: 16),
Expanded(
child: _buildUploadOption(
icon: Icons.image_outlined,
label: '相册上传',
onTap: () {
controller.pickImage(title, ImageSource.gallery);
Get.back();
},
),
),
],
),
],
),
),
),
);
}
// 构建弹窗内的选择按钮
Widget _buildUploadOption({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 24),
decoration: BoxDecoration(
color: const Color(0xFFF2F9F7), // 浅绿色背景
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: Color(0xFF017137), // 深绿色圆圈
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.white, size: 28),
),
const SizedBox(height: 12),
Text(
label,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF333333),
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildCertDetailItem(
String label,
String value, {

View File

@@ -221,8 +221,14 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
stateName: '',
addStatus: '',
addStatusName: '',
rejectReason: '',
hasEdit: true,
rejectReason: '',
isTruckAttachment: 0,
hasHydrogenationAttachment: true,
hasDrivingAttachment: true,
isEdit: '',
drivingAttachments: [],
hydrogenationAttachments: [], gunNumber: '',
);
//打开预约列表
@@ -253,7 +259,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
if (_debounce?.isActive ?? false) {
return;
}
_debounce = Timer(const Duration(seconds: 1), () {});
_debounce = Timer(const Duration(milliseconds: 200), () {});
showLoading("加载中");
@@ -393,7 +399,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
// 创建一个每1分钟执行一次的周期性定时器
_refreshTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
getSiteList();
getSiteList(showloading: false);
});
}
@@ -521,8 +527,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
updateUi();
}
void getSiteList() async {
if (StorageService.to.phone == "13888888888") {
void getSiteList({showloading = true}) async {
if (StorageService.to.phone == "13344444444") {
//该账号给stationOptions手动添加一个数据
final testStation = StationModel(
hydrogenId: '1142167389150920704',
@@ -546,7 +552,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
}
try {
if (showloading) {
showLoading("加氢站数据加载中");
}
var responseData = await HttpService.to.get(
"appointment/station/queryHydrogenSiteInfo",

View File

@@ -49,7 +49,7 @@ class ReservationPage extends GetView<C_ReservationController> {
Positioned(
left: 20.w,
right: 20.w,
bottom: 110.h,
bottom: Get.height * (Get.height < 826 ? 0.08 : 0.11),
child: _buildReservationItem(context),
),
],
@@ -457,27 +457,34 @@ class ReservationPage extends GetView<C_ReservationController> {
/// 时间 Slider 选择器
Widget _buildTimeSlider(BuildContext context) {
return Obx(() {
// 获取当前小时作为滑块值 (0-23)
int currentHour = controller.startTime.value.hour;
// 动态获取站点的营业范围限制
// 获取站点信息
final station = controller.stationOptions.firstWhereOrNull(
(s) => s.hydrogenId == controller.selectedStationId.value,
);
// 解析营业时间
// 处理格式如 "09:00" 或 "09:00:00"
final startParts = (station?.startBusiness ?? "00:00").split(':');
final endParts = (station?.endBusiness ?? "23:59").split(':');
// 如果没有站点数据,默认隐藏
if (station == null) {
return const SizedBox(height: 100);
}
// 解析营业范围
final startParts = station.startBusiness.split(':');
final endParts = station.endBusiness.split(':');
int bizStartHour = int.tryParse(startParts[0]) ?? 0;
int bizEndHour = int.tryParse(endParts[0]) ?? 23;
int bizEndMinute = (endParts.length > 1) ? (int.tryParse(endParts[1]) ?? 0) : 0;
if (bizEndMinute == 0 && bizEndHour > bizStartHour) bizEndHour--;
// 优化结束小时逻辑
// 如果分钟为 0 (例如 18:00),说明该小时整点已关门,最大可选小时应减 1
if (bizEndMinute == 0 && bizEndHour > 0) {
bizEndHour--;
}
//确定当前滑块值
int currentHour = controller.startTime.value.hour;
double sliderValue = currentHour.toDouble().clamp(
bizStartHour.toDouble(),
bizEndHour.toDouble(),
);
double minVal = bizStartHour.toDouble();
double maxVal = bizEndHour.toDouble();
if (minVal >= maxVal) maxVal = minVal + 1;
return Column(
children: [
@@ -526,12 +533,9 @@ class ReservationPage extends GetView<C_ReservationController> {
overlayColor: const Color(0xFF006633).withOpacity(0.1),
),
child: Slider(
value: currentHour.toDouble().clamp(
bizStartHour.toDouble(),
bizEndHour.toDouble(),
),
min: bizStartHour.toDouble(),
max: bizEndHour.toDouble(),
value: sliderValue,
min: minVal,
max: maxVal,
// divisions: bizEndHour - bizStartHour > 0 ? bizEndHour - bizStartHour : 1,
onChanged: (val) {
int hour = val.toInt();

View File

@@ -29,8 +29,14 @@ class HomeController extends GetxController with BaseControllerMixin {
@override
void onInit() {
super.onInit();
// 检查是否同意过隐私政策,只有同意后才初始化推送
if (StorageService.to.isPrivacyAgreed) {
requestPermission();
initAliyunPush();
addPushCallback();
}
FlutterNativeSplash.remove();
log('page-init');
@@ -152,7 +158,6 @@ class HomeController extends GetxController with BaseControllerMixin {
// 根据登录状态和登录渠道返回不同的首页
Widget getHomePage() {
requestPermission();
if (StorageService.to.isLoggedIn) {
if (StorageService.to.loginChannel == LoginChannel.station) {
return B_BaseWidgetsPage();

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart';
@@ -17,6 +18,8 @@ import 'package:ln_jq_app/pages/login/controller.dart';
import 'package:ln_jq_app/pages/url_host/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import '../c_page/message/view.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@@ -30,6 +33,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
bool _obscureText = true;
bool _rememberPassword = true;
bool _credentialsLoaded = false;
bool isPushInitialized = false;
@override
void initState() {
@@ -388,13 +392,28 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
content: _buildDialogContent(),
confirmText: '同意',
cancelText: '拒绝',
onConfirm: () {
onConfirm: () async {
_isAgreed = true;
controller.updateUi();
// 保存隐私政策同意状态
await StorageService.to.savePrivacyAgreed(true);
// 申请通知权限
await _requestNotificationPermission();
// 初始化阿里云推送
await _initPushService();
},
);
return;
}
// 如果已经同意过,但推送还没初始化,则初始化
if (!isPushInitialized) {
await _initPushService();
}
_tabController.index == 0
? _handleDriverLogin(controller)
: _handleStationLogin(controller);
@@ -536,6 +555,62 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
addAlias(identifier);
}
// 申请通知权限
Future<void> _requestNotificationPermission() async {
final PermissionStatus status = await Permission.notification.request();
if (status.isGranted) {
Logger.d('通知权限已授予');
} else if (status.isPermanentlyDenied) {
Logger.d('通知权限被永久拒绝');
}
}
// 初始化推送服务
Future<void> _initPushService() async {
try {
final _aliyunPush = AliyunPushFlutter();
// 初始化推送
final String appKey = Platform.isIOS ? AppTheme.ios_key : AppTheme.android_key;
final String appSecret = Platform.isIOS
? AppTheme.ios_appsecret
: AppTheme.android_appsecret;
final result = await _aliyunPush.initPush(appKey: appKey, appSecret: appSecret);
if (result['code'] != kAliyunPushSuccessCode) {
Logger.d('推送初始化失败: ${result['errorMsg']}');
return;
}
// 配置平台特定设置
if (Platform.isIOS) {
await _aliyunPush.showIOSNoticeWhenForeground(true);
} else if (Platform.isAndroid) {
await _aliyunPush.setNotificationInGroup(true);
await _aliyunPush.createAndroidChannel(
"xll_push_android",
'新消息通知',
4,
'用于接收加氢站实时状态提醒',
);
}
// 添加推送回调
_aliyunPush.addMessageReceiver(
onNotificationOpened: _onNotificationOpened,
);
isPushInitialized = true;
Logger.d('推送服务初始化成功');
} catch (e) {
Logger.d('推送服务初始化异常: $e');
}
}
Future<void> _onNotificationOpened(Map<dynamic, dynamic> message) async {
await Get.to(() => const MessagePage());
}
final _aliyunPush = AliyunPushFlutter();
void addAlias(String alias) async {

View File

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

View File

@@ -798,7 +798,7 @@ packages:
source: hosted
version: "1.1.0"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@@ -957,6 +957,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.3"
saver_gallery:
dependency: "direct main"
description:
name: saver_gallery
sha256: "1d942bd7f4fedc162d9a751e156ebac592e4b81fc2e757af82de9077f3437003"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.0"
shared_preferences:
dependency: transitive
description:

View File

@@ -16,12 +16,12 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.2.3+6
version: 1.2.4+7
environment:
sdk: ^3.9.0
# Dependencies specify other packages that your package needs in order to work.
# Dependencies specify other packages tha。。。t your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
@@ -53,7 +53,8 @@ dependencies:
aliyun_push_flutter: ^1.3.6
pull_to_refresh: ^2.0.0
flutter_app_update: ^3.2.2
saver_gallery: ^4.0.0
path_provider: ^2.1.5
dev_dependencies:
flutter_test: