Compare commits

...

11 Commits

Author SHA1 Message Date
15fdbc7043 v1.2.1 2025-12-23 10:43:12 +08:00
f2f2348b54 逆地理编码 2025-12-23 10:18:44 +08:00
9ba152b3c3 v1.2.1 待发布 2025-12-22 16:15:32 +08:00
21a528d6d1 调整和修改 2025-12-19 13:18:04 +08:00
62ca3888d3 历史统计数据,错误提示 2025-12-18 15:20:54 +08:00
3ec56a925c 修改搜索条件 2025-12-18 10:33:24 +08:00
95c08818cb 取消dark主题 2025-12-18 09:03:08 +08:00
42355bd1ef 站点-新增历史记录 2025-12-17 17:40:02 +08:00
fe2ce75cec 修改绑定车辆接口
登录后查询车辆信息
2025-12-17 09:18:34 +08:00
98cac8a0a5 预约列表增加筛选条件 2025-12-16 16:18:28 +08:00
9ce46a0c7d 加氢站默认选择,演示账号数据
附件默认显示,详情
2025-12-16 11:58:00 +08:00
21 changed files with 1551 additions and 611 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -137,7 +137,7 @@
<!-- 2. 加载地图和插件 (去掉了 Geolocation 插件,避免弹窗) -->
<script
src="https://webapi.amap.com/maps?v=2.0&key=2cc1d822e313307fe311c3127a1deeb5&plugin=AMap.MoveAnimation,AMap.Driving,AMap.TruckDriving,AMap.AutoComplete,AMap.ToolBar,AMap.Scale">
src="https://webapi.amap.com/maps?v=2.0&key=2cc1d822e313307fe311c3127a1deeb5&plugin=AMap.MoveAnimation,AMap.Driving,AMap.TruckDriving,AMap.AutoComplete,AMap.ToolBar,AMap.Scale,AMap.Geocoder">
</script>
</head>
@@ -167,9 +167,11 @@
</div>
<script>
var map, marker, driving, truckDriving;
var map, marker, driving, truckDriving, geocoder;
var currentLat, currentLng;
var isTruckMode = false;
var isInitialLocationSet = false;
function initMap() {
map = new AMap.Map('container', {
@@ -178,6 +180,11 @@
viewMode: '3D'
});
// --- 2. 初始化 geocoder ---
geocoder = new AMap.Geocoder({
city: "全国" // 设置地理编码范围
});
// 通知 Flutter 地图加载完毕
map.on('complete', function () {
console.log("JS->: Map is ready.");
@@ -230,6 +237,7 @@
/**
* 核心功能 1: 接收 Flutter 传来的定位数据
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
* 经度 维度
*/
function updateMyLocation(lat, lng, angle) {
var rawLat = parseFloat(lat);
@@ -244,18 +252,17 @@
currentLat = mPoint.lat;
var position = [currentLng, currentLat];
// 更新车辆标记位置 (保持不变)
if (!marker) {
marker = new AMap.Marker({
map: map,
position: position,
icon: "https://webapi.amap.com/images/car.png",
offset: new AMap.Pixel(-26, -13),
icon: "car.png",
offset: new AMap.Pixel(-23.5, -15),
autoRotation: true,
angle: isNaN(rawAngle) ? 0 : rawAngle,
});
map.setCenter(position);
} else {
marker.moveTo(position, {
duration: 1000,
@@ -263,6 +270,61 @@
});
if (!isNaN(rawAngle)) marker.setAngle(rawAngle);
}
// --- 4. 逆地理编码并设置默认起点 ---
// 只有在第一次获取到位置时,才设置默认起点,避免覆盖用户手动输入的起点
if (!isInitialLocationSet) {
geocoder.getAddress(position, function (status, result) {
if (status === 'complete' && result.regeocode) {
let shortAddress = '';
const regeo = result.regeocode;
const addressComponent = regeo.addressComponent;
const pois = regeo.pois;
// 策略1: 优先使用最近的、类型合适的POI的名称
if (pois && pois.length > 0) {
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI这类POI通常是具体的建筑或地点名
const significantPoi = pois.find(p => p.type.indexOf('商务住宅') === -
1 && p.type.indexOf('地名地址信息') === -1);
if (significantPoi) {
shortAddress = significantPoi.name;
} else {
// 如果找不到就用第一个POI的名字
shortAddress = pois[0].name;
}
}
// 策略2: 如果没有POI使用"道路+门牌号"
else if (addressComponent.street && addressComponent.streetNumber) {
shortAddress = addressComponent.street + addressComponent
.streetNumber;
}
// 策略3: 如果还没有,使用"区+乡镇"
else if (addressComponent.district) {
shortAddress = addressComponent.district + (addressComponent
.township || '');
}
// 策略4: 降级到使用完整的、但可能很长的地址
else {
shortAddress = regeo.formattedAddress;
}
// 如果拼接出的地址过长,可以再做一次截断
if (shortAddress.length > 20) {
// 可以在这里添加更复杂的截断逻辑,比如按关键字
shortAddress = regeo.formattedAddress.substring(0, 20) + '...';
}
// 将获取到的地址填充到起点输入框
document.getElementById('startInput').value = shortAddress;
isInitialLocationSet = true; // 标记为已设置,不再更新
} else {
// 如果逆地理编码失败,依然使用“当前位置”作为提示
document.getElementById('startInput').placeholder = "当前位置";
console.error('逆地理编码失败:', result);
}
});
}
}
});
}

View File

@@ -20,7 +20,9 @@ PODS:
- FlutterMacOS
- image_picker_ios (0.0.1):
- Flutter
- MTBBarcodeScanner (5.0.11)
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
@@ -29,9 +31,6 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- qr_code_scanner_plus (0.2.6):
- Flutter
- MTBBarcodeScanner
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -47,16 +46,15 @@ DEPENDENCIES:
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- qr_code_scanner_plus (from `.symlinks/plugins/qr_code_scanner_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
trunk:
- MTBBarcodeScanner
- OrderedSet
EXTERNAL SOURCES:
@@ -76,14 +74,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/geolocator_apple/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
qr_code_scanner_plus:
:path: ".symlinks/plugins/qr_code_scanner_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
@@ -95,15 +93,14 @@ SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_pdfview: 54e283d5851b0b247b3cc57877d35f1a05a204de
flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
qr_code_scanner_plus: 7e087021bc69873140e0754750eb87d867bed755
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b

View File

@@ -294,10 +294,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -370,10 +374,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -489,7 +497,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -497,7 +505,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.0;
MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -684,7 +692,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -692,7 +700,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.0;
MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -716,7 +724,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -724,7 +732,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.0;
MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -0,0 +1,216 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
class HistoryController extends GetxController {
// --- 定义 API 需要的日期格式化器 ---
final DateFormat _apiDateFormat = DateFormat('yyyy-MM-dd');
// 默认查询最近7天
final Rx<DateTime> startDate = DateTime.now().subtract(const Duration(days: 7)).obs;
final Rx<DateTime> endDate = DateTime.now().obs;
final TextEditingController plateNumberController = TextEditingController();
final RxString totalHydrogen = '0 kg'.obs;
final RxString totalCompletions = '0 次'.obs;
final RxList<ReservationModel> historyList = <ReservationModel>[].obs;
final RxBool isLoading = true.obs;
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 = "";
@override
void onInit() {
super.onInit();
final args = Get.arguments as Map<String, dynamic>;
stationName = args['stationName'] as String;
fetchHistoryData();
}
Future<void> getAllOrderCounts() async {
var response = await HttpService.to.post(
"appointment/orderAddHyd/getAllOrderCounts",
data: {
// --- 直接使用 DateFormat 来格式化日期 ---
'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),
'plateNumber': plateNumberController.text,
'stationName': stationName, // 加氢站名称
},
);
if (response == null || response.data == null) {
totalHydrogen.value = '0 kg';
totalCompletions.value = '0 次';
return;
}
try {
final baseModel = BaseModel<dynamic>.fromJson(response.data);
final dataMap = baseModel.data as Map<String, dynamic>;
totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0} kg';
totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0}';
} catch (e) {
totalHydrogen.value = '0 kg';
totalCompletions.value = '0 次';
}
}
Future<void> fetchHistoryData() async {
isLoading.value = true;
//获取数据
getAllOrderCounts();
try {
var response = await HttpService.to.post(
"appointment/orderAddHyd/sitOrderPage",
data: {
// --- 直接使用 DateFormat 来格式化日期 ---
'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),
'plateNumber': plateNumberController.text,
'pageNum': 1,
'pageSize': 50,
'stationName': stationName, // 加氢站名称
},
);
if (response == null || response.data == null) {
showToast('无法获取历史记录');
_resetData();
return;
}
final baseModel = BaseModel<dynamic>.fromJson(response.data);
if (baseModel.code == 0 && baseModel.data != null) {
final dataMap = baseModel.data as Map<String, dynamic>;
final List<dynamic> listFromServer = dataMap['records'] ?? [];
historyList.assignAll(
listFromServer
.map((item) => ReservationModel.fromJson(item as Map<String, dynamic>))
.toList(),
);
hasData.value = historyList.isNotEmpty;
} else {
showToast(baseModel.message);
_resetData();
}
} catch (e) {
showToast('获取历史记录失败: $e');
_resetData();
} finally {
isLoading.value = false;
}
}
void _resetData() {
historyList.clear();
hasData.value = false;
}
void pickDate(BuildContext context, bool isStartDate) {
// 确定当前操作的日期和临时存储变量
final DateTime initialDate = isStartDate ? startDate.value : endDate.value;
DateTime tempDate = initialDate;
// 定义全局的最早可选日期
final DateTime globalMinimumDate = DateTime(2025, 12, 1);
// 动态计算当前选择器的最小/最大日期范围
DateTime minimumDate;
DateTime? maximumDate; // 声明为可空,因为两个日期都可能没有最大限制
if (isStartDate) {
// 当选择【开始日期】时 它的最小日期就是全局最小日期
minimumDate = globalMinimumDate;
// 最大日期没有限制
maximumDate = null;
} else {
// 当选择【结束日期】时 它的最小日期不能早于当前的开始日期
minimumDate = startDate.value;
// 确认结束日期没有最大限制 ---
//最大日期没有限制
maximumDate = null;
}
Get.bottomSheet(
Container(
height: 300,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
// 顶部的取消和确认按钮
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消', style: TextStyle(color: Colors.grey)),
),
TextButton(
onPressed: () {
// 4. 确认后,更新对应的日期变量
if (isStartDate) {
startDate.value = tempDate;
// 如果新的开始日期晚于结束日期,自动将结束日期调整为同一天
if (tempDate.isAfter(endDate.value)) {
endDate.value = tempDate;
}
} else {
endDate.value = tempDate;
}
Get.back();
// 选择日期后自动刷新数据
fetchHistoryData();
},
child: const Text(
'确认',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
const Divider(height: 1),
// 日期选择器
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: initialDate,
// 应用动态计算好的最小/最大日期
minimumDate: minimumDate,
maximumDate: maximumDate,
onDateTimeChanged: (DateTime newDate) {
tempDate = newDate;
},
),
),
],
),
),
backgroundColor: Colors.transparent, // 使底部工作表外的区域透明
);
}
@override
void onClose() {
plateNumberController.dispose();
super.onClose();
}
}

View File

@@ -0,0 +1,249 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/history/controller.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
class HistoryPage extends GetView<HistoryController> {
const HistoryPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Get.put(HistoryController());
return Scaffold(
appBar: AppBar(title: const Text('历史记录'), centerTitle: true),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
_buildFilterCard(context),
const SizedBox(height: 12),
_buildSummaryCard(),
const SizedBox(height: 12),
_buildListHeader(),
Expanded(child: _buildHistoryList()),
],
),
),
);
}
Widget _buildFilterCard(BuildContext context) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('时间范围', style: TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 8),
Row(
children: [
Expanded(child: _buildDateField(context, true)),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text(''),
),
Expanded(child: _buildDateField(context, false)),
],
),
const SizedBox(height: 16),
const Text('车牌号', style: TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 8),
SizedBox(
height: 44,
child: TextField(
controller: controller.plateNumberController,
decoration: InputDecoration(
hintText: '请输入车牌号',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
FocusScope.of(context).unfocus(); // Hide keyboard
controller.fetchHistoryData();
},
icon: const Icon(Icons.search, size: 20),
label: const Text('查询'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 44),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
],
),
),
);
}
Widget _buildSummaryCard() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Obx(
() => Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildSummaryItem('实际加氢总量', controller.totalHydrogen.value, Colors.blue),
const SizedBox(width: 1, height: 40, child: VerticalDivider()),
_buildSummaryItem(
'预约完成次数',
controller.totalCompletions.value,
Colors.green,
),
],
),
),
),
);
}
Widget _buildHistoryList() {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (!controller.hasData.value) {
return const Center(child: Text('没有找到相关记录'));
}
return ListView.builder(
itemCount: controller.historyList.length,
itemBuilder: (context, index) {
final ReservationModel item = controller.historyList[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text('车牌号: ${item.plateNumber}'),
subtitle: Text.rich(
TextSpan(
children: [
TextSpan(
text: '加氢站: ${item.stationName}\n',
style: TextStyle(fontSize: 16),
),
TextSpan(
text: '时间: ${item.time}\n',
style: TextStyle(fontSize: 16),
),
TextSpan(
text: '加氢量:',
),
TextSpan(
text: '${item.amount}',
style: TextStyle(fontSize: 16, color: AppTheme.themeColor),
),
],
),
)
,
trailing:
// 状态标签
_buildStatusChip(item.status),
),
);
},
);
});
}
Widget _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;
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 _buildDateField(BuildContext context, bool isStart) {
return Obx(
() => InkWell(
onTap: () => controller.pickDate(context, isStart),
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(isStart ? controller.formattedStartDate : controller.formattedEndDate),
const Icon(Icons.calendar_today, size: 18, color: Colors.grey),
],
),
),
),
);
}
Widget _buildSummaryItem(String label, String value, Color color) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(color: color, fontSize: 22, fontWeight: FontWeight.bold),
),
],
);
}
Widget _buildListHeader() {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 14.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('加氢明细', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
);
}
}

View File

@@ -191,6 +191,7 @@ class SiteController extends GetxController with BaseControllerMixin {
showToast('暂时无法获取预约数据');
hasReservationData = false;
reservationList = [];
dismissLoading();
return;
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/history/view.dart';
import 'controller.dart';
@@ -114,18 +115,21 @@ class SitePage extends GetView<SiteController> {
Card(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
margin: const EdgeInsets.only(bottom: 12),
margin: EdgeInsets.only(bottom: 12),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Container(
color: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Row(
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
Expanded(
child: GestureDetector(
onTap: () {
controller.renderData();
},
child: Row(
children: [
Text(
'今日预约信息',
@@ -135,27 +139,40 @@ class SitePage extends GetView<SiteController> {
color: Colors.white,
),
),
SizedBox(height: 2),
Text(
'Reservation Information',
style: TextStyle(fontSize: 12, color: Colors.white70),
SizedBox(
width: 32,
height: 32,
child: const Icon(
Icons.refresh,
size: 18,
color: Colors.white,
),
),
],
),
),
ElevatedButton.icon(
onPressed: () {
controller.renderData();
},
icon: const Icon(Icons.refresh, size: 16),
label: const Text('刷新'),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.blue,
backgroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
ElevatedButton(
onPressed: () {
Get.to(
() => HistoryPage(),
arguments: {
'stationName': controller.name,
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
elevation: 2,
),
child: const Text(
'历史记录',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
),
],

View File

@@ -40,11 +40,10 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
if (controller.fileType == 'pdf') {
if (controller.localFilePath.isNotEmpty) {
return PDFView(
key: ValueKey(controller.localFilePath.value),
filePath: controller.localFilePath.value,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
swipeHorizontal: true,
autoSpacing: true,
pageFling: true,
onRender: (pages) {
print("PDF rendered with $pages pages.");

View File

@@ -1,21 +1,77 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:path_provider/path_provider.dart';
import 'attachment_viewer_page.dart';
class CertificateViewerController extends GetxController {
class CertificateViewerController extends GetxController with BaseControllerMixin{
late final String title;
late final List<String> attachments;
// --- 新增: 状态管理 ---
/// 用于存储网络PDF的本地路径key是网络urlvalue是本地路径
final RxMap<String, String> localPdfPaths = <String, String>{}.obs;
/// 用于跟踪每个附件的加载状态key是网络url
final RxMap<String, bool> isLoading = <String, bool>{}.obs;
@override
String get builderId => "certificateviewer";
@override
void onInit() {
super.onInit();
// 从 Get.to 的 arguments 中获取标题和附件列表
title = Get.arguments['title'] ?? '证件详情';
attachments = List<String>.from(Get.arguments['attachments'] ?? []);
// --- 新增: 初始化时开始加载所有PDF ---
_loadAllPdfs();
}
/// 导航到通用的附件查看器页面
/// 遍历所有附件如果是PDF则进行下载
void _loadAllPdfs() {
for (var url in attachments) {
if (isPdf(url)) {
_downloadPdf(url);
}
}
}
/// 下载单个PDF文件
Future<void> _downloadPdf(String url) async {
if (url.isEmpty) return;
// 开始加载
isLoading[url] = true;
try {
final dio = Dio();
final Directory tempDir = await getTemporaryDirectory();
final String savePath = '${tempDir.path}/${url.split('/').last}';
// 检查文件是否已存在,避免重复下载
if (await File(savePath).exists()) {
localPdfPaths[url] = savePath;
isLoading[url] = false;
return;
}
await dio.download(url, savePath);
// 下载成功后,更新本地路径
localPdfPaths[url] = savePath;
} catch (e) {
print('PDF download error for $url: $e');
// 出错时也可以更新状态以便UI显示错误提示
} finally {
// 结束加载
isLoading[url] = false;
}
}
/// 导航到通用的附件查看器页面 (此方法保持不变)
void openAttachment(String url) {
if (url.isEmpty) {
showErrorToast('附件链接无效');
@@ -30,8 +86,10 @@ class CertificateViewerController extends GetxController {
);
}
/// 检查 URL 是否为 PDF,以便在视图中显示不同的图标
/// 检查 URL 是否为 PDF (此方法保持不变)
bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf');
}
}

View File

@@ -1,51 +1,114 @@
import 'package:flutter/material.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart'; // 引入PDFView
import 'package:get/get.dart';
import 'package:getx_scaffold/common/common.dart';
import 'certificate_viewer_controller.dart';
class CertificateViewerPage extends GetView<CertificateViewerController> {
const CertificateViewerPage({Key? key}) : super(key: key);
const CertificateViewerPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(CertificateViewerController());
return GetBuilder<CertificateViewerController>(
init: CertificateViewerController(),
id: 'certificateviewer',
builder: (_) {
return Scaffold(
appBar: AppBar(
title: Text(controller.title),
centerTitle: true,
appBar: AppBar(title: Text(controller.title)),
body: Column(
children: [
SizedBox(height: 16,),
Text(
"点击可查看大图",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
body: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: controller.attachments.length,
itemBuilder: (context, index) {
final url = controller.attachments[index];
// 从 URL 中提取文件名用于显示
final fileName = url.split('/').last;
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: Icon(
controller.isPdf(url)
? Icons.picture_as_pdf_rounded // PDF 图标
: Icons.image_rounded, // 图片图标
color: controller.isPdf(url) ? Colors.red.shade700 : Colors.blue.shade700,
size: 32,
return _buildAttachmentItem(url);
},
),
title: Text(
fileName,
style: const TextStyle(fontSize: 14),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16, color: Colors.grey),
onTap: () => controller.openAttachment(url),
],
),
);
},
);
}
/// 构建单个附件的显示项
Widget _buildAttachmentItem(String url) {
return GestureDetector(
onTap: () {
controller.openAttachment(url);
}, // 点击跳转到详情页
child: Card(
margin: const EdgeInsets.only(bottom: 16.0),
clipBehavior: Clip.antiAlias, // 确保内容不会溢出Card的圆角
elevation: 4,
// 等比缩放展示
child: AspectRatio(
aspectRatio: 4 / 3,
child: controller.isPdf(url)
? Obx(() {
final bool loading = controller.isLoading[url] ?? true;
final String? localPath = controller.localPdfPaths[url];
if (loading) {
return _buildLoadingIndicator();
} else if (localPath != null && localPath.isNotEmpty) {
return IgnorePointer(
ignoring: true, // 设置为 true 来忽略所有指针事件
child: PDFView(
fitEachPage: true,
filePath: localPath,
fitPolicy: FitPolicy.WIDTH,
// 适配宽度
enableSwipe: false,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
preventLinkNavigation: true, // 顺便禁用PDF内部链接的跳转
),
);
} else {
// PDF加载失败
return _buildErrorIndicator();
}
})
: Image.network(
url,
fit: BoxFit.contain,
// 图片加载时显示loading
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildLoadingIndicator();
},
// 图片加载失败时显示错误
errorBuilder: (context, error, stackTrace) {
return _buildErrorIndicator();
},
),
),
),
);
}
// 辅助Widget加载中指示器
Widget _buildLoadingIndicator() {
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
}
// 辅助Widget错误指示器
Widget _buildErrorIndicator() {
return const SizedBox(
height: 200,
child: Center(child: Icon(Icons.error_outline, color: Colors.red, size: 48)),
);
}
}

View File

@@ -28,11 +28,14 @@ class CarInfoController extends GetxController with BaseControllerMixin {
super.onInit();
getUserBindCarInfo();
}
@override
void onReady() {
super.onReady();
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
if (!StorageService.to.hasShownBindVehicleDialog && StorageService.to.isLoggedIn) {
if (!StorageService.to.hasShownBindVehicleDialog &&
StorageService.to.isLoggedIn &&
!StorageService.to.hasVehicleInfo) {
Future.delayed(const Duration(milliseconds: 500), () {
DialogX.to.showConfirmDialog(
title: '当前尚未绑定车辆',
@@ -88,7 +91,9 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// 将解析出的 URL 列表赋值给对应的 RxList
drivingAttachments.assignAll(parseAttachments(data['drivingAttachment']));
operationAttachments.assignAll(parseAttachments(data['operationAttachment']));
hydrogenationAttachments.assignAll(parseAttachments(data['hydrogenationAttachment']));
hydrogenationAttachments.assignAll(
parseAttachments(data['hydrogenationAttachment']),
);
registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
}
}
@@ -98,7 +103,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
/// 跳转到证件查看页面
void navigateToCertificateViewer(String title, List<String> attachments) {
if(!StorageService.to.hasVehicleInfo){
if (!StorageService.to.hasVehicleInfo) {
showToast('请先绑定车辆');
return;
}
@@ -108,10 +113,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
}
Get.to(
() => const CertificateViewerPage(),
arguments: {
'title': title,
'attachments': attachments,
},
arguments: {'title': title, 'attachments': attachments},
);
}
}

View File

@@ -434,7 +434,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
//打开预约列表
Future.delayed(const Duration(milliseconds: 500), () {
getReservationList();
getReservationList(showPopup: true, addStatus: '');
});
} else {
showToast(result.error);
@@ -446,16 +446,18 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
}
/// 状态变量:是否有预约数据
bool hasReservationData = false;
final RxBool hasReservationData = false.obs;
// 新增预约数据列表
List<ReservationModel> reservationList = [];
final RxList<ReservationModel> reservationList = <ReservationModel>[].obs;
final RxBool shouldShowReservationList = false.obs;
// --- 用于防抖的 Timer ---
Timer? _debounce;
//查看预约列表
void getReservationList() async {
void getReservationList({bool showPopup = false, String? addStatus}) async {
// 增加 addStatus 参数
if (_debounce?.isActive ?? false) {
return;
}
@@ -464,19 +466,25 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
showLoading("加载中");
try {
final Map<String, dynamic> requestData = {
'phone': StorageService.to.phone,
'pageNum': 1,
'pageSize': 50,
};
// 将 addStatus 参数添加到请求中
if (addStatus != null && addStatus.isNotEmpty) {
requestData['addStatus'] = addStatus;
}
var response = await HttpService.to.post(
"appointment/orderAddHyd/driverOrderPage",
data: {
'phone': StorageService.to.phone, // 使用从 renderData 中获取到的 name
'pageNum': 1,
'pageSize': 50, // 暂时不考虑分页一次获取30条
},
data: requestData,
);
if (response == null || response.data == null) {
showToast('暂时无法获取预约数据');
hasReservationData = false;
reservationList = [];
hasReservationData.value = false;
reservationList.clear();
return;
}
@@ -485,13 +493,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
if (baseModel.code == 0 && baseModel.data != null) {
final dataMap = baseModel.data as Map<String, dynamic>;
final List<dynamic> listFromServer = dataMap['records'] ?? [];
reservationList = listFromServer.map((item) {
// 使用 .value 来更新响应式列表
reservationList.value = listFromServer.map((item) {
return ReservationModel.fromJson(item as Map<String, dynamic>);
}).toList();
// 根据列表是否为空来更新 hasReservationData 状态
hasReservationData = reservationList.isNotEmpty;
// 更新 hasEdit 状态
for (var reservation in reservationList) {
try {
// 获取当前时间和预约的结束时间
@@ -510,205 +518,24 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
reservation.hasEdit = false;
}
}
hasReservationData.value = reservationList.isNotEmpty;
if (showPopup) {
shouldShowReservationList.value = true;
}
} else {
showToast(baseModel.message);
hasReservationData = false;
reservationList = []; // 清空列表
hasReservationData.value = false;
reservationList.clear();
}
} catch (e) {
showToast('获取预约数据失败');
hasReservationData = false;
reservationList = []; // 清空列表
hasReservationData.value = false;
reservationList.clear();
} finally {
dismissLoading();
}
Get.bottomSheet(
Container(
height: Get.height * 0.55,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
//标题
Container(
padding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'我的预约',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
child: const Text('关闭', style: TextStyle(color: Colors.black54)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: !hasReservationData
? Container(
margin: EdgeInsets.only(top: 40),
child: TextX.bodyLarge('暂无预约', weight: FontWeight.w500),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: reservationList.length,
itemBuilder: (context, index) {
final ReservationModel reservation = reservationList[index];
return Card(
color: Colors.white,
margin: const EdgeInsets.only(bottom: 12.0),
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: const Color(
0xFFE6F7FF,
), // Light blue background
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: const Color(0xFF91D5FF),
), // Blue border
),
child: Text(
reservation.stateName +
"-" +
reservation.addStatusName,
style: const TextStyle(
color: Color(0xFF1890FF),
fontWeight: FontWeight.bold,
),
),
),
!reservation.hasEdit
? SizedBox()
: GestureDetector(
onTap: () async {
var result = await Get.to(
() => ReservationEditPage(),
arguments: {
'reservation': reservation,
'difference': difference,
},
binding: BindingsBuilder(() {
Get.put(ReservationEditController());
}),
preventDuplicates: false,
);
if (result == true) {
Get.back();
getReservationList();
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(
0xFFFFF7E6,
), // Light orange background
borderRadius: BorderRadius.circular(12),
),
child: Text(
"修改",
style: const TextStyle(
color: Color(0xFFFA8C16),
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
),
],
),
const SizedBox(height: 12),
_buildDetailRow('车牌号:', reservation.plateNumber),
_buildDetailRow('预约日期:', reservation.date),
_buildDetailRow('预约氢量:', reservation.hydAmount),
_buildDetailRow('加氢站:', reservation.stationName),
_buildDetailRow('开始时间:', reservation.startTime),
_buildDetailRow('结束时间:', reservation.endTime),
_buildDetailRow('联系人:', reservation.contacts),
_buildDetailRow('联系电话:', reservation.phone),
reservation.addStatus == "5"
? _buildDetailRow('拒绝原因:', reservation.rejectReason)
: SizedBox(),
],
),
),
);
},
),
),
],
),
),
isScrollControlled: true,
backgroundColor:
Colors.transparent, // Make background transparent to see the rounded corners
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 85,
child: Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
),
Expanded(
child: Text(
value,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: Colors.black87,
),
),
),
],
),
);
}
String workEfficiency = "0";
@@ -809,7 +636,28 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
}
void getSiteList() async {
showLoading("加载中");
if (StorageService.to.phone == "13344444444") {
//该账号给stationOptions手动添加一个数据
final testStation = StationModel(
hydrogenId: '1142167389150920704',
name: '羚牛氢能演示加氢站',
address: '上海市嘉定区于田南路111号于田大厦',
price: '35.00',
// 价格
siteStatusName: '营运中',
// 状态
isSelect: 1, // 默认可选
);
// 使用 assignAll 可以确保列表只包含这个测试数据
stationOptions.assignAll([testStation]);
if (stationOptions.isNotEmpty) {
selectedStationId.value = stationOptions.first.hydrogenId;
}
return;
}
showLoading("加氢站数据加载中");
final originalHeaders = Map<String, dynamic>.from(HttpService.to.dio.options.headers);
try {
HttpService.to.setBaseUrl(AppTheme.jiaqing_service_url);
@@ -822,8 +670,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
dismissLoading();
return;
}
try {
dismissLoading();
var result = BaseModel.fromJson(responseData.data);
var stationDataList = result.data['data'] as List;
@@ -849,17 +695,30 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
} else {
showToast('站点列表已刷新');
}
} catch (e) {
showToast('数据异常');
// 找到第一个可选的站点作为默认值
if (stationOptions.isNotEmpty) {
final firstSelectable = stationOptions.firstWhere(
(station) => station.isSelect == 1,
orElse: () => stationOptions.first, // 降级:如果没有可选的,就用第一个
);
selectedStationId.value = firstSelectable.hydrogenId;
} else {
// 如果列表为空,确保 selectedStationId 也为空
selectedStationId.value = null;
}
} catch (e) {
dismissLoading();
showToast('数据异常');
} finally {
dismissLoading();
HttpService.to.setBaseUrl(AppTheme.test_service_url);
HttpService.to.dio.options.headers = originalHeaders;
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
if (!StorageService.to.hasShownBindVehicleDialog && StorageService.to.isLoggedIn) {
if (!StorageService.to.hasShownBindVehicleDialog &&
StorageService.to.isLoggedIn &&
!StorageService.to.hasVehicleInfo) {
Future.delayed(const Duration(milliseconds: 500), () {
DialogX.to.showConfirmDialog(
title: '当前尚未绑定车辆',

View File

@@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/common/index.dart';
import 'package:ln_jq_app/pages/c_page/reservation/controller.dart';
import 'package:ln_jq_app/pages/c_page/reservation_edit/controller.dart';
import 'package:ln_jq_app/pages/c_page/reservation_edit/view.dart';
//加氢预约列表
class ReservationListBottomSheet extends StatefulWidget {
const ReservationListBottomSheet({super.key});
@override
State<ReservationListBottomSheet> createState() => _ReservationListBottomSheetState();
}
class _ReservationListBottomSheetState extends State<ReservationListBottomSheet> {
final C_ReservationController _controller = Get.find<C_ReservationController>();
final Map<String, String> _statusOptions = {
'': '所有状态', // 增加一个“所有状态”的选项
'0': '待加氢',
'1': '加氢完成',
'2': '未加氢',
'5': '拒绝加氢',
};
String _selectedStatus = ''; // 默认选中 '0' (待加氢)
@override
void initState() {
super.initState();
// Widget 初始化时,立即调用接口加载默认状态(待加氢)的数据
_controller.getReservationList(addStatus: _selectedStatus);
}
@override
Widget build(BuildContext context) {
return Container(
height: Get.height * 0.55,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
// 构建标题和下拉框
_buildHeader(),
const Divider(height: 1),
// 下拉筛选框
_buildChoice(),
// 构建列表(使用 Obx 监听数据变化)
_buildList(),
],
),
);
}
Container _buildChoice() {
return Container(
padding: const EdgeInsets.fromLTRB(20, 8, 0, 0),
alignment: AlignmentGeometry.centerLeft,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: DropdownButton<String>(
value: _selectedStatus,
underline: const SizedBox.shrink(), // 隐藏下划线
items: _statusOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(entry.value),
);
}).toList(),
onChanged: (newValue) {
if (newValue != null) {
setState(() {
_selectedStatus = newValue;
});
// 当选择新状态时,调用接口刷新数据
_controller.getReservationList(addStatus: _selectedStatus);
}
},
),
),
);
}
/// 构建标题、关闭按钮和下拉筛选框
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.fromLTRB(20, 8, 8, 8),
child: Stack(
alignment: Alignment.center,
children: [
Center(child: const Text('我的预约', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
),
child: const Text('关闭', style: TextStyle(color: Colors.black54)),
),
),
],
),
);
}
/// 构建预约列表
Widget _buildList() {
return Expanded(
child: Obx(() {
if (!_controller.hasReservationData.value) {
return Container(
margin: const EdgeInsets.only(top: 40),
child: TextX.bodyLarge('暂无该状态下的预约', weight: FontWeight.w500),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _controller.reservationList.length,
itemBuilder: (context, index) {
final reservation = _controller.reservationList[index];
return Card(
color: Colors.white,
margin: const EdgeInsets.only(bottom: 12.0),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 状态标签
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: const Color(0xFFE6F7FF),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: const Color(0xFF91D5FF)),
),
child: Text(
"${reservation.stateName}-${reservation.addStatusName}",
style: const TextStyle(
color: Color(0xFF1890FF),
fontWeight: FontWeight.bold,
),
),
),
// 修改按钮 (仅在 hasEdit 为 true 时显示)
if (reservation.hasEdit)
GestureDetector(
onTap: () async {
var result = await Get.to(
() => ReservationEditPage(),
arguments: {
'reservation': reservation,
'difference': _controller.difference,
},
binding: BindingsBuilder(() {
Get.put(ReservationEditController());
}),
preventDuplicates: false,
);
if (result == true) {
_controller.getReservationList(
addStatus: _selectedStatus,
);
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFFFFF7E6),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
"修改",
style: TextStyle(
color: Color(0xFFFA8C16),
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
),
],
),
const SizedBox(height: 12),
_buildDetailRow('车牌号:', reservation.plateNumber),
_buildDetailRow('预约日期:', reservation.date),
_buildDetailRow('预约氢量:', reservation.hydAmount),
_buildDetailRow('加氢站:', reservation.stationName),
_buildDetailRow('开始时间:', reservation.startTime),
_buildDetailRow('结束时间:', reservation.endTime),
_buildDetailRow('联系人:', reservation.contacts),
_buildDetailRow('联系电话:', reservation.phone),
reservation.addStatus == "5"
? _buildDetailRow('拒绝原因:', reservation.rejectReason)
: SizedBox(),
],
),
),
);
},
);
}),
);
}
/// 构建详情行 (这是一个辅助Widget)
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Text(label, style: const TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
Expanded(
child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
),
],
),
);
}
}

View File

@@ -7,10 +7,13 @@ import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import 'controller.dart';
import 'reservation_list_bottomsheet.dart';
///加氢预约
class ReservationPage extends GetView<C_ReservationController> {
const ReservationPage({super.key});
ReservationPage({super.key});
bool init = false;
@override
Widget build(BuildContext context) {
@@ -18,6 +21,10 @@ class ReservationPage extends GetView<C_ReservationController> {
init: C_ReservationController(),
id: 'reservation',
builder: (_) {
if (!init) {
_setupListener(context);
init = true;
}
return Scaffold(
backgroundColor: Colors.grey[100],
body: GestureDetector(
@@ -299,7 +306,9 @@ class ReservationPage extends GetView<C_ReservationController> {
const SizedBox(width: 16),
Expanded(
child: OutlinedButton(
onPressed: controller.getReservationList,
onPressed: () {
controller.getReservationList(showPopup: true, addStatus: '');
},
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 38), // 高度与另一个按钮保持一致
side: const BorderSide(color: Colors.blue),
@@ -326,6 +335,21 @@ class ReservationPage extends GetView<C_ReservationController> {
);
}
void _setupListener(BuildContext context) {
ever(controller.shouldShowReservationList, (bool shouldShow) {
if (shouldShow) {
Get.bottomSheet(
const ReservationListBottomSheet(),
isScrollControlled: true, // 允许弹窗使用更多屏幕高度
backgroundColor: Colors.transparent,
);
// 重要:显示后立即将信号重置为 false防止不必要的重复弹出
controller.shouldShowReservationList.value = false;
}
});
}
// 表单中的可点击行 (用于日期和时间选择)
Widget _buildPickerRow({
required String label,
@@ -437,20 +461,40 @@ class ReservationPage extends GetView<C_ReservationController> {
),
)
.toList(),
value: controller.selectedStationId.value,
value:
// 当前的站点 处理默认
controller.selectedStationId.value ??
(controller.stationOptions.isNotEmpty
? controller.stationOptions.first.hydrogenId
: null),
// 当前选中的是站点ID
onChanged: (value) {
if (value != null) {
controller.selectedStationId.value = value;
}
},
customButton: controller.selectedStationId.value == null
? null // 未选择时,显示默认的 hint
: _buildSelectedStationButton(
controller.stationOptions.firstWhere(
customButton: Obx(() {
// 优先从已选中的 ID 查找
var selectedStation = controller.stationOptions.firstWhereOrNull(
(s) => s.hydrogenId == controller.selectedStationId.value,
),
),
);
// 如果找不到已选中的(比如 ID 为空或列表里没有),并且列表不为空,则取第一个作为默认
final stationToShow =
selectedStation ??
(controller.stationOptions.isNotEmpty
? controller.stationOptions.first
: null);
// 如果有要显示的站点,就构建按钮
if (stationToShow != null) {
return _buildSelectedStationButton(stationToShow);
}
// 否则,返回一个空占位符,让 hint 生效
// DropdownButton2 内部会判断,如果 customButton 返回的不是一个有效Widget或根据其内部逻辑就会显示 hint
return const SizedBox.shrink();
}),
buttonStyleData: ButtonStyleData(
height: 40, // 增加高度以容纳两行文字
padding: const EdgeInsets.symmetric(horizontal: 12.0),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
@@ -174,8 +175,16 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
showToast('登录失败:无法获取凭证');
return;
}
//登录信息处理
try {
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
return;
}
String token = result.data['token'] ?? '';
String idCard = result.data['idCard'] ?? '';
String name = result.data['name'] ?? '';
@@ -188,6 +197,22 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
name: name,
phone: phone,
);
//登录后查询已绑定车辆信息
var carInfo = await HttpService.to.get(
"appointment/driver/getTruckInfoByDriver?phone=$phone"
);
if (carInfo != null) {
var carInforesult = BaseModel.fromJson(carInfo.data);
if (carInforesult.data != null) {
final vehicle = VehicleInfo.fromJson(carInforesult.data as Map<String, dynamic>);
//保存使用
await StorageService.to.saveVehicleInfo(vehicle);
}
}
//页面操作
dismissLoading();
showToast('登录成功,欢迎您');
Get.offAll(() => BaseWidgetsPage());
@@ -319,6 +344,13 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
try {
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
return;
}
String token = result.data['token'] ?? '';
String userId = result.data['userId'] ?? '';

View File

@@ -1,18 +1,13 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:image/image.dart' as img;
import 'package:image_picker/image_picker.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/storage_service.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import 'package:zxing_lib/common.dart';
import 'package:zxing_lib/qrcode.dart';
import 'package:zxing_lib/zxing.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:permission_handler/permission_handler.dart';
class QrCodeController extends GetxController
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
@@ -23,11 +18,11 @@ class QrCodeController extends GetxController
late final AnimationController animationController;
late final Animation<double> scanAnimation;
// --- QR Scanning ---
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
QRViewController? qrViewController;
final Rx<Barcode?> result = Rx<Barcode?>(null);
// --- 使用 MobileScanner 的控制器 ---
final MobileScannerController scannerController = MobileScannerController();
final RxBool isFlashOn = false.obs;
final RxBool isProcessingResult = false.obs;
@override
void onInit() {
@@ -38,171 +33,206 @@ class QrCodeController extends GetxController
duration: const Duration(milliseconds: 2500),
vsync: this,
);
scanAnimation = Tween<double>(begin: 0, end: 1).animate(animationController);
scanAnimation =
Tween<double>(begin: 0, end: 1).animate(animationController);
animationController.repeat(reverse: false);
}
/// 当 QRView 创建时调用
void onQRViewCreated(QRViewController controller) {
this.qrViewController = controller;
// 监听扫描到的数据
controller.scannedDataStream.listen((scanData) {
if (scanData.code != null && result.value?.code != scanData.code) {
result.value = scanData;
qrViewController?.pauseCamera();
/// MobileScanner 的 onDetect 回调方法
void onDetect(BarcodeCapture capture) {
if (isProcessingResult.value) return;
final Barcode? barcode = capture.barcodes.firstOrNull;
if (barcode?.rawValue != null && barcode!.rawValue!.isNotEmpty) {
isProcessingResult.value = true;
scannerController.stop();
animationController.stop();
renderResult(scanData.code!);
print("相机识别到的内容: ${barcode.rawValue!}");
renderResult(barcode.rawValue!);
}
});
}
/// 恢复扫描状态
void resumeScanner() {
result.value = null;
qrViewController?.resumeCamera();
isProcessingResult.value = false;
try {
scannerController.start();
} catch (e) {
print("无法重启相机: $e");
}
animationController.repeat(reverse: false);
}
/// 从相册选择图片并扫描二维码
void scanFromGallery() async {
try {
final XFile? imageFile = await ImagePicker().pickImage(source: ImageSource.gallery);
if (imageFile == null) return; // 用户取消了选择
final XFile? imageFile =
await ImagePicker().pickImage(source: ImageSource.gallery);
if (imageFile == null) {
return;
}
qrViewController?.pauseCamera();
scannerController.stop();
animationController.stop();
showLoading("正在识别...");
String? scanResult;
try {
final image = img.decodeImage(await File(imageFile.path).readAsBytes());
if (image != null) {
//扫描图片
final pixels = Int32List.fromList(
image.map((pixel) {
return (pixel.a.toInt() << 24) |
(pixel.r.toInt() << 16) |
(pixel.g.toInt() << 8) |
pixel.b.toInt();
}).toList(),
);
final BarcodeCapture? capture =
await scannerController.analyzeImage(imageFile.path);
final source = RGBLuminanceSource(image.width, image.height, pixels);
dismissLoading();
final bitmap = BinaryBitmap(HybridBinarizer(source));
final reader = QRCodeReader();
final result = reader.decode(bitmap);
scanResult = result.text;
}
} on NotFoundException {
scanResult = null;
} catch (e) {
//异常
scanResult = null;
}
final Barcode? firstBarcode = capture?.barcodes.firstOrNull;
if (scanResult != null) {
if (firstBarcode?.rawValue != null &&
firstBarcode!.rawValue!.isNotEmpty) {
final String scanResult = firstBarcode.rawValue!;
print("相册识别到的内容: $scanResult");
renderResult(scanResult);
} else {
showErrorToast('未识别到二维码');
resumeScanner();
}
} catch (e) {
showErrorToast('从相册选择失败');
} catch (e, stackTrace) {
dismissLoading();
showErrorToast('从相册选择失败,请稍后重试');
print("scanFromGallery Error: $e\n$stackTrace");
resumeScanner();
}
}
/// 切换闪光灯
void toggleFlash() async {
await qrViewController?.toggleFlash();
isFlashOn.value = (await qrViewController?.getFlashStatus()) ?? false;
try {
await scannerController.toggleTorch();
final currentTorchState = scannerController.value.torchState;
isFlashOn.value = currentTorchState == TorchState.on;
} catch (e) {
print("切换闪光灯失败: $e");
showErrorToast("无法打开闪光灯");
}
}
/// 翻转相机
void flipCamera() async {
await qrViewController?.flipCamera();
await scannerController.switchCamera();
}
/// 请求相机权限
void requestPermission() async {
if(Platform.isIOS){
var status = await Permission.camera.request();
if (status.isGranted) {
}
else if (status.isPermanentlyDenied) {
openAppSettings();
}
else {
showErrorToast('需要相机权限才能扫描二维码');
}
return;
}
final bool results = await requestCameraPermission();
if (!results) {
showErrorToast('相机权限未被授予,请到权限管理中打开');
if (!status.isGranted) {
showErrorToast('请授予相机权限以使用扫描功能');
Get.back();
}
}
void requestPhotoPermission() async {
if (Platform.isAndroid) {
final bool results = await requestPhotosPermission();
if (!results) {
showErrorToast('相册权限未被授予,请到权限管理中打开');
} else {
scanFromGallery();
}
}
if(Platform.isIOS){
var status = await Permission.photos.request();
print("权限状态: $status"); // 在控制台看这个输出
if (status.isGranted) {
scanFromGallery();
}
else if (status.isPermanentlyDenied) {
} else if (status.isPermanentlyDenied) {
openAppSettings();
}
else {
} else {
showErrorToast('需要相册权限才能从相册中选择图片');
}
}
}
//扫码结果处理
void renderResult(String resultStr) async {
/// 处理扫描结果
void renderResult(String resultStr, {plateNumber}) async {
showLoading("正在获取车辆信息...");
try {
var responseData = await HttpService.to.get(
"appointment/truck/base-info?vin=$resultStr",
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 || responseData.data == null) {
if (responseData == null) {
dismissLoading();
showToast('无法获取车辆信息,请检查网络或稍后重试');
resumeScanner();
return;
}
var result = BaseModel.fromJson(responseData.data);
final vehicle = VehicleInfo.fromJson(result.data as Map<String, dynamic>);
//保存使用
await StorageService.to.saveVehicleInfo(vehicle);
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 (e) {
} on DioException catch (_) {
showErrorToast("网络请求失败,请稍后重试");
resumeScanner();
} catch (e, stackTrace) {
showErrorToast("处理失败,请稍后重");
resumeScanner(); // 未知异常,恢复扫描
} catch (e, _) {
showErrorToast("处理失败,请稍后重");
resumeScanner();
} finally {
if (Get.isDialogOpen ?? false) {
dismissLoading();
}
}
}
/// 显示绑定确认对话框
void showBindDialog(String resultStr) {
final TextEditingController plateNumberController = TextEditingController();
// 使用 showConfirmDialog它有 onCancel 回调
DialogX.to.showConfirmDialog(
title: '请输入车牌号',
barrierDismissible: false,
content: TextField(
controller: plateNumberController,
autofocus: false,
decoration: const InputDecoration(
hintText: '请输入完整的车牌号',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12.0),
),
),
confirmText: '确认绑定',
cancelText: '取消', // showConfirmDialog 有 cancelText
onConfirm: () {
final String plateNumber = plateNumberController.text.trim();
if (plateNumber.isEmpty) {
showToast("请输入车牌号");
// 返回 false 可以阻止弹窗关闭,让用户继续输入
return false;
}
renderResult(resultStr, plateNumber: plateNumber);
//关闭弹窗
return true;
},
onCancel: () {
// 如果用户点击取消,恢复扫描
resumeScanner();
},
);
}
@override
void onClose() {
qrViewController?.dispose();
scannerController.dispose();
animationController.dispose();
super.onClose();
}
}

View File

@@ -1,19 +1,16 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'controller.dart';
class QrCodePage extends GetView<QrCodeController> {
const QrCodePage({Key? key}) : super(key: key);
const QrCodePage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<QrCodeController>(
init: QrCodeController(),
id: 'qrcode',
builder: (_) {
Get.put(QrCodeController());
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
@@ -28,85 +25,89 @@ class QrCodePage extends GetView<QrCodeController> {
actions: [
TextButton(
onPressed: controller.requestPhotoPermission,
child: const Text(
'相册',
style: TextStyle(color: Colors.white, fontSize: 16),
),
child: const Text('相册', style: TextStyle(color: Colors.white, fontSize: 16)),
),
],
),
body: Stack(
children: <Widget>[
_buildQrView(context),
Positioned(
bottom: 80.h,
left: 0,
right: 0,
child: _buildControlButtons(),
alignment: Alignment.center,
children: [
// 1. 使用 MobileScanner 作为扫描视图
MobileScanner(
controller: controller.scannerController,
onDetect: controller.onDetect,
// 您可以自定义扫描框的样式
scanWindow: Rect.fromCenter(
center: Offset(
MediaQuery.of(context).size.width / 2,
MediaQuery.of(context).size.height / 2 - 50,
),
width: 250,
height: 250,
),
),
// 扫描动画和覆盖层
_buildScannerOverlay(context),
// 底部的功能按钮
Positioned(bottom: 80, left: 0, right: 0, child: _buildActionButtons()),
],
),
);
},
);
}
/// 构建二维码扫描视图(带动画
Widget _buildQrView(BuildContext context) {
// 定义扫描区域的大小
var scanArea = (MediaQuery.of(context).size.width < 400 ||
MediaQuery.of(context).size.height < 400)
? 250.0
: 300.0;
/// 构建扫描区域的覆盖层和动画
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,
children: <Widget>[
// 底层是相机视图和半透明遮罩
QRView(
key: controller.qrKey,
onQRViewCreated: controller.onQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Colors.blueAccent,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: scanArea,
child: Container(
margin: const EdgeInsets.only(bottom: 100), // 微调位置
width: scanAreaSize,
height: scanAreaSize,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12),
),
),
// 上层是扫描动画
AnimatedBuilder(
),
],
),
),
// 扫描动画
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 Positioned(
// 计算扫描框的顶部位置,以便动画从顶部开始
top: (MediaQuery.of(context).size.height - scanArea) / 2,
child: Transform.translate(
offset: Offset(0, controller.scanAnimation.value * scanArea),
child: Container(
width: scanArea,
height: 2, // 扫描线的高度
decoration: BoxDecoration(
color: Colors.blueAccent,
boxShadow: [
BoxShadow(
color: Colors.blueAccent.withOpacity(0.7),
blurRadius: 8,
spreadRadius: 2,
),
],
),
),
return CustomPaint(
painter: ScannerAnimationPainter(
controller.scanAnimation.value,
AppTheme.themeColor,
),
);
},
),
),
),
],
);
}
/// 构建底部的控制按钮
Widget _buildControlButtons() {
/// 构建底部的功能按钮(闪光灯、相册)
Widget _buildActionButtons() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -123,11 +124,16 @@ class QrCodePage extends GetView<QrCodeController> {
_buildIconButton(
onPressed: controller.toggleFlash,
//闪光灯状态的变化
child: Obx(() => Icon(
child: Obx(
() => IconButton(
icon: Icon(
controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off,
color: Colors.white,
size: 28,
)),
),
onPressed: controller.toggleFlash,
),
),
),
// 翻转相机按钮
_buildIconButton(
@@ -144,6 +150,7 @@ class QrCodePage extends GetView<QrCodeController> {
);
}
Widget _buildIconButton({required VoidCallback onPressed, required Widget child}) {
return Container(
decoration: BoxDecoration(
@@ -158,3 +165,71 @@ class QrCodePage extends GetView<QrCodeController> {
);
}
}
/// 扫描动画的绘制器
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

@@ -65,14 +65,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
charset:
dependency: transitive
description:
name: charset
sha256: "27802032a581e01ac565904ece8c8962564b1070690794f0072f6865958ce8b9"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
clock:
dependency: transitive
description:
@@ -399,10 +391,10 @@ packages:
dependency: "direct main"
description:
name: flutter_pdfview
sha256: a9055bf920c7095bf08c2781db431ba23577aa5da5a056a7152dc89a18fbec6f
sha256: c0b2cc4ebf461a5a4bb9312a165222475a7d93845c7a0703f4abb7f442eb6d54
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.2"
version: "1.4.3"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -733,6 +725,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.1.4"
modal_bottom_sheet:
dependency: transitive
description:
@@ -925,14 +925,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.3"
qr_code_scanner_plus:
dependency: "direct main"
description:
name: qr_code_scanner_plus
sha256: b764e5004251c58d9dee0c295e6006e05bd8d249e78ac3383abdb5afe0a996cd
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.14"
rational:
dependency: transitive
description:
@@ -1234,14 +1226,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.3"
zxing_lib:
dependency: "direct main"
description:
name: zxing_lib
sha256: f9170470b6bc947d21a6783486f88ef48aad66fc1380c8acd02b118418ec0ce0
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.4"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0"

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.2.0+3
version: 1.2.1+4
environment:
sdk: ^3.9.0
@@ -43,11 +43,10 @@ dependencies:
flutter_native_splash: ^2.4.7
dropdown_button2: ^2.3.8
qr_code_scanner_plus: ^2.0.14
image_picker: ^1.2.1 # 用于从相册选择图片
image: ^4.5.4
zxing_lib: ^1.1.4
flutter_pdfview: 1.3.2 #显示pdf
mobile_scanner: ^7.1.4
flutter_pdfview: 1.4.3 #显示pdf
photo_view: ^0.15.0 #操作图片
flutter_inappwebview: ^6.1.5 # WebView插件
geolocator: ^14.0.2 # 获取精确定位