13 Commits

Author SHA1 Message Date
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
45f5035d1b 更换logo 2026-01-19 10:13:37 +08:00
f792915429 bugfix 2026-01-16 17:47:53 +08:00
edbacc502b 线上域名修改 2026-01-16 17:25:52 +08:00
46 changed files with 498 additions and 563 deletions

View File

@@ -37,8 +37,8 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = 5 versionCode = 6
versionName = "1.2.2" versionName = "1.2.3"
} }
signingConfigs { signingConfigs {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -25,6 +25,41 @@
display: none !important; 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 { #panel .amap-call {
display: none; display: none;
} }
@@ -32,7 +67,7 @@
/* --- 搜索栏样式 --- */ /* --- 搜索栏样式 --- */
#search-box { #search-box {
position: absolute; position: absolute;
top: 10px; top: 40px;
left: 10px; left: 10px;
right: 10px; right: 10px;
z-index: 100; z-index: 100;
@@ -63,7 +98,7 @@
button { button {
padding: 0 15px; padding: 0 15px;
height: 38px; height: 38px;
background: #3366FF; background: #017143FF;
color: #fff; color: #fff;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
@@ -74,7 +109,7 @@
/* --- 导航结果面板 (底部弹出) --- */ /* --- 导航结果面板 (底部弹出) --- */
#panel { #panel {
position: fixed; position: fixed;
bottom: 0; bottom: 75px;
left: 0; left: 0;
width: 100%; width: 100%;
height: 35%; height: 35%;
@@ -94,7 +129,7 @@
#location-btn { #location-btn {
position: fixed; position: fixed;
right: 10px; right: 10px;
bottom: 50px; bottom: 75px;
/* 默认位置 */ /* 默认位置 */
width: 44px; width: 44px;
height: 44px; height: 44px;
@@ -121,9 +156,24 @@
fill: #555; 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 类时,按钮上移 --- */
body.panel-active #location-btn { body.panel-active #location-btn {
bottom: 38%; bottom: 45%;
/* 对应 #panel 的 height + 一点间距 */ /* 对应 #panel 的 height + 一点间距 */
} }
</style> </style>
@@ -167,7 +217,7 @@
</div> </div>
<script> <script>
var map, marker, driving, truckDriving, geocoder; var map, marker, destMarker, driving, truckDriving, geocoder;
var currentLat, currentLng; var currentLat, currentLng;
var isTruckMode = false; var isTruckMode = false;
var isInitialLocationSet = false; var isInitialLocationSet = false;
@@ -176,7 +226,7 @@
function initMap() { function initMap() {
map = new AMap.Map('container', { map = new AMap.Map('container', {
resizeEnable: true, resizeEnable: true,
zoom: 15, zoom: 17,
viewMode: '3D' viewMode: '3D'
}); });
@@ -237,7 +287,7 @@
/** /**
* 核心功能 1: 接收 Flutter 传来的定位数据 * 核心功能 1: 接收 Flutter 传来的定位数据
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)") * Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
* *
*/ */
function updateMyLocation(lat, lng, angle) { function updateMyLocation(lat, lng, angle) {
var rawLat = parseFloat(lat); var rawLat = parseFloat(lat);
@@ -263,6 +313,7 @@
angle: isNaN(rawAngle) ? 0 : rawAngle, angle: isNaN(rawAngle) ? 0 : rawAngle,
}); });
map.setCenter(position); map.setCenter(position);
map.setZoom(13);
} else { } else {
marker.moveTo(position, { marker.moveTo(position, {
duration: 1000, duration: 1000,
@@ -281,6 +332,10 @@
const addressComponent = regeo.addressComponent; const addressComponent = regeo.addressComponent;
const pois = regeo.pois; const pois = regeo.pois;
console.log("地理:" + JSON.stringify(result));
fetchStationInfo(addressComponent.province, addressComponent.city,
addressComponent.district, lat, lng);
// 策略1: 优先使用最近的、类型合适的POI的名称 // 策略1: 优先使用最近的、类型合适的POI的名称
if (pois && pois.length > 0) { if (pois && pois.length > 0) {
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI这类POI通常是具体的建筑或地点名 // 查找第一个类型不是“商务住宅”或“地名地址信息”的POI这类POI通常是具体的建筑或地点名
@@ -295,8 +350,9 @@
} }
// 策略2: 如果没有POI使用"道路+门牌号" // 策略2: 如果没有POI使用"道路+门牌号"
else if (addressComponent.street && addressComponent.streetNumber) { else if (addressComponent.street && addressComponent.streetNumber) {
shortAddress = addressComponent.street + addressComponent shortAddress = addressComponent.district +
.streetNumber; addressComponent.township +
addressComponent.street + addressComponent.streetNumber;
} }
// 策略3: 如果还没有,使用"区+乡镇" // 策略3: 如果还没有,使用"区+乡镇"
else if (addressComponent.district) { 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() { function backToLocation() {
if (currentLng && currentLat) { if (currentLng && currentLat) {
@@ -346,7 +499,7 @@
} }
/** /**
* 核心功能 3: 路径规划 * 路径规划
*/ */
function startRouteSearch() { function startRouteSearch() {
// 获取输入框的文字 // 获取输入框的文字

View File

@@ -13,7 +13,7 @@
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "Icon-App-29x29@1x.png", "filename" : "Icon-App-29x29 1.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "1x", "scale" : "1x",
"size" : "29x29" "size" : "29x29"
@@ -31,25 +31,25 @@
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "Icon-App-80x80.jpg", "filename" : "Icon-App-80x80.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "Icon-App-120x120.jpg", "filename" : "Icon-App-120x120.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "Icon-App-120x120 1.jpg", "filename" : "Icon-App-120x120 1.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "60x60" "size" : "60x60"
}, },
{ {
"filename" : "Icon-App-180.jpg", "filename" : "Icon-App-180.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "60x60" "size" : "60x60"
@@ -61,19 +61,19 @@
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "Icon-App-20x20@2x.png", "filename" : "Icon-App-20x20@2x 1.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "Icon-App-29x29@1x.png", "filename" : "Icon-App-29x29.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "1x", "scale" : "1x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "Icon-App-29x29@2x.png", "filename" : "Icon-App-29x29@2x 1.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "29x29" "size" : "29x29"
@@ -91,7 +91,7 @@
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "Icon-App-76x76@1x.png", "filename" : "Icon-App-76x76.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "1x", "scale" : "1x",
"size" : "76x76" "size" : "76x76"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 B

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -70,5 +70,11 @@
<string>fetch</string> <string>fetch</string>
</array> </array>
<key>CFBundleLocalizations</key>
<array>
<string>zh-Hans</string>
<string>en</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -8,11 +8,11 @@ class AppTheme {
static const Color themeColor = Color(0xFF017137); static const Color themeColor = Color(0xFF017137);
//是否开放域名切换 //是否开放域名切换
static const bool is_show_host = true; static const bool is_show_host = false;
//http://192.168.110.222:8080/ //http://192.168.110.222:8080/
//http://192.168.110.44:8080/ //http://192.168.110.44:8080/
static String test_service_url = "https://beta-esg.api.lnh2e.com/"; static String test_service_url = "http://47.101.201.13:8443/api/";
static const String release_service_url = ""; static const String release_service_url = "";
//加氢站相关查询 //加氢站相关查询

View File

@@ -16,7 +16,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
WidgetsBinding widgetsBinding = await init( WidgetsBinding widgetsBinding = await init(
isDebug: true, isDebug: false,
logTag: '小羚羚', logTag: '小羚羚',
supportedLocales: [const Locale('zh', 'CN')], supportedLocales: [const Locale('zh', 'CN')],
); );
@@ -34,7 +34,7 @@ void main() async {
// 设计稿尺寸 单位dp // 设计稿尺寸 单位dp
designSize: const Size(390, 844), designSize: const Size(390, 844),
// Getx Log // Getx Log
enableLog: true, enableLog: false,
// 默认的跳转动画 // 默认的跳转动画
defaultTransition: Transition.rightToLeft, defaultTransition: Transition.rightToLeft,
// 主题模式 // 主题模式
@@ -69,6 +69,9 @@ void initHttpSet() {
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token')); HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
HttpService.to.setOnResponseHandler((response) async { HttpService.to.setOnResponseHandler((response) async {
try { try {
if (response.data == null) {
return null;
}
final baseModel = BaseModel.fromJson(response.data); final baseModel = BaseModel.fromJson(response.data);
if (baseModel.code == 0 || baseModel.code == 200) { if (baseModel.code == 0 || baseModel.code == 200) {
return null; return null;

View File

@@ -91,10 +91,10 @@ class B_BaseWidgetsPage extends GetView<B_BaseWidgetsController> {
id: 'b_baseWidgets', id: 'b_baseWidgets',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
extendBody: false, extendBody: true,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
bottomNavigationBar: _buildNavigationBar(), bottomNavigationBar: _buildNavigationBar(),
body: SafeArea(child: _buildView()), body: _buildView(),
); );
}, },
); );

View File

@@ -45,7 +45,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
customStartTime = DateTime.now(); customStartTime = DateTime.now();
customEndTime = customStartTime!.add(const Duration(days: 1)); customEndTime = customStartTime!.add(const Duration(days: 1));
renderData(); renderData();
_msgNotice(); // 红点消息 msgNotice(); // 红点消息
startAutoRefresh(); startAutoRefresh();
} }
@@ -248,7 +248,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
} }
} }
Future<void> _msgNotice() async { Future<void> msgNotice() async {
final Map<String, dynamic> requestData = { final Map<String, dynamic> requestData = {
'appFlag': 1, 'appFlag': 1,
'isRead': 1, 'isRead': 1,
@@ -264,6 +264,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
if (result.code == 0 && result.data != null) { if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString(); String total = result.data["total"].toString();
isNotice = int.parse(total) > 0; isNotice = int.parse(total) > 0;
updateUi();
} }
} }
} }

View File

@@ -36,7 +36,7 @@ class ReservationPage extends GetView<ReservationController> {
_buildSystemTips(), _buildSystemTips(),
SizedBox(height: 24), SizedBox(height: 24),
_buildLogoutButton(), _buildLogoutButton(),
SizedBox(height: 40), SizedBox(height: 75.h),
], ],
), ),
), ),
@@ -98,8 +98,12 @@ class ReservationPage extends GetView<ReservationController> {
), ),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () async{
Get.to(() => const MessagePage()); var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
}, },
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.grey[100], backgroundColor: Colors.grey[100],
@@ -359,9 +363,9 @@ class ReservationPage extends GetView<ReservationController> {
child: Text( child: Text(
status, status,
style: TextStyle( style: TextStyle(
fontSize: 11.sp, fontSize: 13.sp,
color: isSelected ? Colors.white : Color.fromRGBO(51, 51, 51, 1), color: isSelected ? Colors.white : Color.fromRGBO(51, 51, 51, 1),
fontWeight: FontWeight.w400, fontWeight: FontWeight.w500,
), ),
), ),
), ),

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

View File

@@ -4,6 +4,7 @@ import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/history/view.dart'; import 'package:ln_jq_app/pages/b_page/history/view.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart'; import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'controller.dart'; import 'controller.dart';
@@ -19,7 +20,12 @@ class SitePage extends GetView<SiteController> {
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
backgroundColor: Color.fromRGBO(247, 249, 251, 1), backgroundColor: Color.fromRGBO(247, 249, 251, 1),
body: SingleChildScrollView(child: _buildView(context)), body: SmartRefresher(
controller: controller.refreshController,
enablePullUp: false,
onRefresh: controller.onRefresh,
child: SingleChildScrollView(child: _buildView(context)),
),
); );
}, },
); );
@@ -127,10 +133,10 @@ class SitePage extends GetView<SiteController> {
], ],
), ),
), ),
SizedBox(height: 35.h),
], ],
), ),
), ),
SizedBox(height: 75.h),
], ],
); );
} }
@@ -180,8 +186,11 @@ class SitePage extends GetView<SiteController> {
), ),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () async {
Get.to(() => const MessagePage()); var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
}, },
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.grey[100], backgroundColor: Colors.grey[100],
@@ -491,7 +500,7 @@ class SitePage extends GetView<SiteController> {
"预约量:${item.amount}", "预约量:${item.amount}",
style: TextStyle( style: TextStyle(
color: Color(0xFF00A870), color: Color(0xFF00A870),
fontSize: 12.sp, fontSize: 13.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@@ -506,7 +515,7 @@ class SitePage extends GetView<SiteController> {
"${item.contactPerson} | ${item.contactPhone}", "${item.contactPerson} | ${item.contactPhone}",
style: TextStyle( style: TextStyle(
color: Color(0xFF999999), color: Color(0xFF999999),
fontSize: 12.sp, fontSize: 13.sp,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
), ),

View File

@@ -34,10 +34,10 @@ class CarInfoController extends GetxController with BaseControllerMixin {
void onInit() { void onInit() {
super.onInit(); super.onInit();
getUserBindCarInfo(); getUserBindCarInfo();
_msgNotice(); msgNotice();
} }
Future<void> _msgNotice() async { Future<void> msgNotice() async {
final Map<String, dynamic> requestData = { final Map<String, dynamic> requestData = {
'appFlag': 1, 'appFlag': 1,
'isRead': 1, 'isRead': 1,
@@ -53,6 +53,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
if (result.code == 0 && result.data != null) { if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString(); String total = result.data["total"].toString();
isNotice = int.parse(total) > 0; isNotice = int.parse(total) > 0;
updateUi();
} }
} }
} }

View File

@@ -133,8 +133,11 @@ class CarInfoPage extends GetView<CarInfoController> {
), ),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () async{
Get.to(() => const MessagePage()); var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
}, },
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.grey[100], backgroundColor: Colors.grey[100],
@@ -345,7 +348,7 @@ class CarInfoPage extends GetView<CarInfoController> {
), ),
const SizedBox(height: 9), const SizedBox(height: 9),
SizedBox( SizedBox(
height: 336.h, // 给定一个高度,或者使用别的方式布局 height: 356.h, // 给定一个高度,或者使用别的方式布局
child: TabBarView( child: TabBarView(
children: [ children: [
_buildCertificateContent('行驶证', controller.drivingAttachments), _buildCertificateContent('行驶证', controller.drivingAttachments),
@@ -379,7 +382,7 @@ class CarInfoPage extends GetView<CarInfoController> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: true), _buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: false),
_buildCertDetailItem('运营城市', controller.address), _buildCertDetailItem('运营城市', controller.address),
], ],
), ),

View File

@@ -140,8 +140,12 @@ class MinePage extends GetView<MineController> {
), ),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () async{
Get.to(() => const MessagePage()); var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
}, },
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.grey[100], backgroundColor: Colors.grey[100],

View File

@@ -33,6 +33,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
@override @override
String get builderId => 'reservation'; String get builderId => 'reservation';
C_ReservationController();
final DateTime _now = DateTime.now(); final DateTime _now = DateTime.now();
// 计算当前时间属于哪个1小时区间 // 计算当前时间属于哪个1小时区间
@@ -342,7 +344,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
return; return;
}*/ }*/
final reservationEndDateTime = DateTime( DateTime reservationEndDateTime = DateTime(
selectedDate.value.year, selectedDate.value.year,
selectedDate.value.month, selectedDate.value.month,
selectedDate.value.day, selectedDate.value.day,
@@ -350,7 +352,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
endTime.value.minute, 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( if (reservationEndDateTime.isBefore(
DateTime.now().subtract(const Duration(minutes: 1)), DateTime.now().subtract(const Duration(minutes: 1)),
)) { )) {
@@ -363,10 +371,10 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
(s) => s.hydrogenId == selectedStationId.value, (s) => s.hydrogenId == selectedStationId.value,
); );
if (selectedStation.siteStatusName != "营运中") { /*if (selectedStation.siteStatusName != "营运中") {
showToast("该站点${selectedStation.siteStatusName},暂无法预约"); showToast("该站点${selectedStation.siteStatusName},暂无法预约");
return; return;
} }*/
showLoading("提交中"); showLoading("提交中");
@@ -552,7 +560,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
getUserBindCarInfo(); getUserBindCarInfo();
getSiteList(); getSiteList();
startAutoRefresh(); startAutoRefresh();
_msgNotice(); msgNotice();
if (!init) { if (!init) {
_setupListener(); _setupListener();
@@ -562,7 +570,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
bool isNotice = false; bool isNotice = false;
Future<void> _msgNotice() async { Future<void> msgNotice() async {
final Map<String, dynamic> requestData = { final Map<String, dynamic> requestData = {
'appFlag': 1, 'appFlag': 1,
'isRead': 1, 'isRead': 1,
@@ -578,16 +586,11 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
if (result.code == 0 && result.data != null) { if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString(); String total = result.data["total"].toString();
isNotice = int.parse(total) > 0; isNotice = int.parse(total) > 0;
updateUi();
} }
} }
} }
@override
void onPaused() {
stopAutoRefresh();
super.onPaused();
}
void startAutoRefresh() { void startAutoRefresh() {
// 先停止已存在的定时器,防止重复启动 // 先停止已存在的定时器,防止重复启动
stopAutoRefresh(); stopAutoRefresh();
@@ -656,8 +659,10 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
var result = BaseModel.fromJson(responseData.data); var result = BaseModel.fromJson(responseData.data);
fillingWeight = final value = double.tryParse(result.data["fillingWeight"]?.toString() ?? '0') ?? 0;
"${result.data["fillingWeight"]}${result.data["fillingWeightUnit"]}"; final String formatted = value.toStringAsFixed(2);
fillingWeight = "$formatted${result.data["fillingWeightUnit"]}";
fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}"; fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}";
updateUi(); updateUi();
@@ -776,7 +781,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
if (stationOptions.isEmpty) { if (stationOptions.isEmpty) {
showToast('附近暂无可用加氢站'); showToast('附近暂无可用加氢站');
} else { } else {
showToast('站点列表已刷新'); // showToast('站点列表已刷新');
} }
// 找到第一个可选的站点作为默认值 // 找到第一个可选的站点作为默认值
@@ -824,6 +829,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
@override @override
void onClose() { void onClose() {
super.onClose();
amountController.dispose(); amountController.dispose();
plateNumberController.dispose(); plateNumberController.dispose();
if (_debounce != null) { if (_debounce != null) {
@@ -831,6 +837,5 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
} }
_sheetWorker?.dispose(); _sheetWorker?.dispose();
stopAutoRefresh(); stopAutoRefresh();
super.onClose();
} }
} }

View File

@@ -1,17 +1,12 @@
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart'; import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/common/model/station_model.dart'; import 'package:ln_jq_app/common/model/station_model.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart'; import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'controller.dart'; import 'controller.dart';
import 'reservation_list_bottomsheet.dart';
///加氢预约 ///加氢预约
class ReservationPage extends GetView<C_ReservationController> { class ReservationPage extends GetView<C_ReservationController> {
@@ -152,7 +147,12 @@ class ReservationPage extends GetView<C_ReservationController> {
), ),
), ),
IconButton( IconButton(
onPressed: () => Get.to(() => const MessagePage()), onPressed: () async {
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
icon: Badge( icon: Badge(
smallSize: 8, smallSize: 8,
backgroundColor: controller.isNotice backgroundColor: controller.isNotice
@@ -519,7 +519,7 @@ class ReservationPage extends GetView<C_ReservationController> {
color: Colors.white, color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(13.0), padding: EdgeInsets.all(13.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -566,19 +566,27 @@ class ReservationPage extends GetView<C_ReservationController> {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
IconButton( IconButton(
onPressed: () => _updateAmount(-1), onPressed: () => _updateAmount(-1),
icon: const Icon(Icons.remove, size: 18, color: Colors.grey), icon: Icon(Icons.remove, size: 14.sp, color: Colors.grey),
), ),
Text( Expanded(
"${controller.amountController.text} Kg", child: Text(
style: const TextStyle(fontSize: 14, color: Colors.black87), "${controller.amountController.text}Kg",
textAlign: TextAlign.center,
softWrap: false,
overflow: TextOverflow.fade,
style: TextStyle(
fontSize: 11.sp,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
), ),
IconButton( IconButton(
onPressed: () => _updateAmount(1), onPressed: () => _updateAmount(1),
icon: const Icon(Icons.add, size: 18, color: Colors.grey), icon: Icon(Icons.add, size: 14.sp, color: Colors.grey),
), ),
], ],
), ),
@@ -758,6 +766,9 @@ class ReservationPage extends GetView<C_ReservationController> {
controller.amountController.text = newAmount.toStringAsFixed(2); controller.amountController.text = newAmount.toStringAsFixed(2);
} }
//更新进度条
controller.current = newAmount;
// 移动光标到末尾,防止光标跳到前面 // 移动光标到末尾,防止光标跳到前面
controller.amountController.selection = TextSelection.fromPosition( controller.amountController.selection = TextSelection.fromPosition(
TextPosition(offset: controller.amountController.text.length), TextPosition(offset: controller.amountController.text.length),

View File

@@ -68,13 +68,14 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
resizeToAvoidBottomInset: false,
body: GestureDetector( body: GestureDetector(
onTap: () { onTap: () {
hideKeyboard(); hideKeyboard();
}, },
child: Stack( child: Stack(
children: [ children: [
// 1. 顶部背景与装饰 //顶部背景与装饰
Positioned( Positioned(
top: 0, top: 0,
left: 0, left: 0,
@@ -92,9 +93,9 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
), ),
_buildBrandingHeader(), _buildBrandingHeader(),
// 2. 登录表单主体 // 登录表单主体
Positioned( Positioned(
top: 280.h, top: 300.h,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
@@ -443,12 +444,19 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
data: {'account': account, 'password': encryptedPassword, 'loginType': "station"}, data: {'account': account, 'password': encryptedPassword, 'loginType': "station"},
); );
if (_rememberPassword) { if (responseData != null && responseData.data != null) {
await StorageService.to.saveStationCredentials(account, password); var result = BaseModel.fromJson(responseData.data);
} else { if (result.code == 0) {
await StorageService.to.clearStationCredentials(); // 登录成功,处理记住密码逻辑
if (_rememberPassword) {
await StorageService.to.saveStationCredentials(account, password);
} else {
await StorageService.to.clearStationCredentials();
}
}
} }
_processLoginResponse(responseData, "station", account); _processLoginResponse(responseData, "station", account);
} catch (e) { } catch (e) {
dismissLoading(); dismissLoading();
@@ -475,7 +483,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
) async { ) async {
if (responseData == null || responseData.data == null) { if (responseData == null || responseData.data == null) {
dismissLoading(); dismissLoading();
showToast('登录失败');
return; return;
} }
var result = BaseModel.fromJson(responseData.data); var result = BaseModel.fromJson(responseData.data);
@@ -532,7 +539,16 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
final _aliyunPush = AliyunPushFlutter(); final _aliyunPush = AliyunPushFlutter();
void addAlias(String alias) async { void addAlias(String alias) async {
await _aliyunPush.addAlias(alias); var result = await _aliyunPush.bindAccount(alias);
var code = result['code'];
if (code == kAliyunPushSuccessCode) {
Logger.d('添加别名$alias成功');
} else {
var errorCode = result['code'];
var errorMsg = result['errorMsg'];
Logger.d('添加别名$alias失败: $errorCode - $errorMsg');
}
} }
Widget buildAgreement() { Widget buildAgreement() {

View File

@@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart' as dio;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
@@ -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/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart'; import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
class QrCodeController extends GetxController class QrCodeController extends GetxController with BaseControllerMixin {
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
@override @override
String get builderId => 'qrcode'; String get builderId => 'qrcode';
// --- Animation --- final RxBool hasCameraPermission = false.obs;
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;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
requestPermission(); _checkPermission();
animationController = AnimationController(
duration: const Duration(milliseconds: 2500),
vsync: this,
);
scanAnimation =
Tween<double>(begin: 0, end: 1).animate(animationController);
animationController.repeat(reverse: false);
} }
/// MobileScanner 的 onDetect 回调方法 /// 检查权限
void onDetect(BarcodeCapture capture) { void _checkPermission() async {
if (isProcessingResult.value) return; var status = await Permission.camera.status;
hasCameraPermission.value = status.isGranted;
final Barcode? barcode = capture.barcodes.firstOrNull;
if (barcode?.rawValue != null && barcode!.rawValue!.isNotEmpty) {
isProcessingResult.value = true;
scannerController.stop();
animationController.stop();
renderResult(barcode.rawValue!);
}
} }
/// 恢复扫描状态 /// 1. 拍照并识别车牌流程
void resumeScanner() { void takePhotoAndRecognize() async {
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 {
var status = await Permission.camera.request(); var status = await Permission.camera.request();
hasPermission.value = status.isGranted;
if (!status.isGranted) { if (!status.isGranted) {
if (status.isPermanentlyDenied) { if (status.isPermanentlyDenied) openAppSettings();
showErrorToast('相机权限已被永久拒绝,请到系统设置中开启'); showErrorToast("需要相机权限才能拍照识别");
// 延迟一会再引导用户去设置 return;
Future.delayed(const Duration(seconds: 2), () => openAppSettings());
} else {
showErrorToast('请授予相机权限以使用扫描功能');
}
} }
} final XFile? photo = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80, // 压缩图片质量以加快上传
);
if (photo == null) return;
void requestPhotoPermission() async { // 1.1 上传文件
var status = await Permission.photos.request(); String? imageUrl = await uploadFile(photo.path);
if (status.isGranted) { if (imageUrl != null) {
scanFromGallery(); // 1.2 获取车牌号
} else if (status.isPermanentlyDenied) { String? plateNumber = await getPlateNumber(imageUrl);
openAppSettings(); if (plateNumber != null) {
} else { // 1.3 弹窗确认
showErrorToast('需要相册权限才能从相册中选择图片'); manualInputBind(plateNumber, 0);
}
}
/// 处理扫描结果
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();
} }
} }
} }
/// 显示绑定确认对话框 /// 手动输入车牌绑定
void showBindDialog(String resultStr) { void manualInputBind(String plateNumber, int source) {
final TextEditingController plateNumberController = TextEditingController(); final TextEditingController controller = TextEditingController(
// 使用 showConfirmDialog它有 onCancel 回调 text: plateNumber.toUpperCase() ?? '',
);
DialogX.to.showConfirmDialog( DialogX.to.showConfirmDialog(
title: '输入车牌', title: source == 0 ? "识别结果" : '手动输入车牌',
barrierDismissible: false, content: SizedBox(
content: TextField( height: 40.h,
controller: plateNumberController, child: TextField(
autofocus: false, textAlign: TextAlign.start,
decoration: const InputDecoration( controller: controller,
hintText: '请输入完整的车牌号', autofocus: plateNumber.isEmpty,
border: OutlineInputBorder(), textCapitalization: TextCapitalization.characters,
contentPadding: EdgeInsets.symmetric(horizontal: 12.0), decoration: const InputDecoration(
contentPadding: EdgeInsets.only(left: 5),
hintText: '请输入完整的车牌号',
border: OutlineInputBorder(),
),
), ),
), ),
confirmText: '确认绑定', confirmText: '确认绑定',
cancelText: '取消', // showConfirmDialog 有 cancelText cancelText: source == 0 ? "重新拍摄" : '取消',
onConfirm: () {
final String plateNumber = plateNumberController.text.trim();
if (plateNumber.isEmpty) {
showToast("请输入车牌号");
// 返回 false 可以阻止弹窗关闭,让用户继续输入
return false;
}
renderResult(resultStr, plateNumber: plateNumber);
//关闭弹窗
return true;
},
onCancel: () { onCancel: () {
// 如果用户点击取消,恢复扫描 if (source == 0) {
resumeScanner(); takePhotoAndRecognize();
}
},
onConfirm: () {
final plate = controller.text.trim().toUpperCase();
if (plate.isNotEmpty) {
bindVehicleByPlate(plate);
}
}, },
); );
} }
@override /// 上传图片
void onClose() { Future<String?> uploadFile(String filePath) async {
scannerController.dispose(); showLoading("正在上传图片...");
animationController.dispose(); try {
super.onClose(); 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:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'controller.dart'; import 'controller.dart';
class QrCodePage extends GetView<QrCodeController> { class QrCodePage extends GetView<QrCodeController> {
const QrCodePage({super.key}); const QrCodePage({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Get.put(QrCodeController()); return GetBuilder<QrCodeController>(
return Scaffold( init: QrCodeController(),
extendBodyBehindAppBar: true, id: 'qrcode',
appBar: AppBar( builder: (controller) {
title: const Text('扫码', style: TextStyle(color: Colors.white)), return Scaffold(
centerTitle: true, appBar: AppBar(
backgroundColor: Colors.transparent, title: const Text('绑定车辆'),
elevation: 0, centerTitle: true,
leading: IconButton( elevation: 0,
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)),
), ),
], body: Container(
), width: double.infinity,
body: Obx(() { // 1. 使用 Obx 包裹整个 body padding: const EdgeInsets.symmetric(horizontal: 24.0),
// 根据权限状态来决定显示什么 child: SingleChildScrollView(child: Column(
if (controller.hasPermission.value) { mainAxisAlignment: MainAxisAlignment.center,
// 如果有权限,显示扫描器 children: [
return _buildScannerView(context); const SizedBox(height: 24),
} else { Icon(
// 如果没有权限,显示引导界面 Icons.directions_car_rounded,
return _buildPermissionDeniedView(); size: 100,
} color: Theme.of(context).primaryColor.withOpacity(0.1),
}), ),
); const SizedBox(height: 24),
} const Text(
Widget _buildScannerView(BuildContext context){ "请选择绑定方式",
if (!controller.animationController.isAnimating) { style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
controller.animationController.repeat(reverse: false); ),
} const SizedBox(height: 8),
return Stack( const Text(
alignment: Alignment.center, "您可以拍摄照片自动识别,\n或手动输入车牌号进行绑定。",
children: [ textAlign: TextAlign.center,
// 使用 MobileScanner 作为扫描视图 style: TextStyle(fontSize: 14, color: Colors.grey),
MobileScanner( ),
controller: controller.scannerController, const SizedBox(height: 60),
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()),
],
);
}
Widget _buildPermissionDeniedView() { // 拍照识别按钮
// 确保动画是停止的 ElevatedButton.icon(
if (controller.animationController.isAnimating) { onPressed: controller.takePhotoAndRecognize,
controller.animationController.stop(); icon: const Icon(Icons.camera_alt_rounded),
} label: const Text("拍照识别车牌"),
style: ElevatedButton.styleFrom(
return Container( minimumSize: const Size(double.infinity, 56),
color: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
alignment: Alignment.center, textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
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),
), ),
), ),
), const SizedBox(height: 20),
],
),
),
// 扫描动画
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,
),
);
},
),
),
),
],
);
}
/// 构建底部的功能按钮(闪光灯、相册) // 3. 手动输入按钮
Widget _buildActionButtons() { OutlinedButton.icon(
return Column( onPressed: (){
mainAxisSize: MainAxisSize.min, controller.manualInputBind("",1);
children: [ },
const Text( icon: const Icon(Icons.edit_note_rounded),
'将二维码/条形码放入框内,即可自动扫描', label: const Text("手动输入车牌"),
style: TextStyle(color: Colors.white, fontSize: 14), style: OutlinedButton.styleFrom(
), minimumSize: const Size(double.infinity, 56),
const SizedBox(height: 20), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
Row( side: BorderSide(color: Theme.of(context).primaryColor, width: 1.5),
mainAxisAlignment: MainAxisAlignment.spaceEvenly, textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
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,
), ),
), ),
), const SizedBox(height: 100), // 底部留白
// 翻转相机按钮 ],
_buildIconButton( ),),
onPressed: controller.flipCamera,
child: const Icon(
Icons.flip_camera_ios,
color: Colors.white,
size: 28,
),
),
],
), ),
], );
); },
}
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

@@ -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 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.2.2+5 version: 1.2.3+6
environment: environment:
sdk: ^3.9.0 sdk: ^3.9.0