diff --git a/ln_jq_app/assets/html/car.png b/ln_jq_app/assets/html/car.png new file mode 100644 index 0000000..5f5ba0a Binary files /dev/null and b/ln_jq_app/assets/html/car.png differ diff --git a/ln_jq_app/assets/html/map.html b/ln_jq_app/assets/html/map.html index 3ef4926..3df0453 100644 --- a/ln_jq_app/assets/html/map.html +++ b/ln_jq_app/assets/html/map.html @@ -248,8 +248,8 @@ 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, }); diff --git a/ln_jq_app/lib/pages/b_page/history/controller.dart b/ln_jq_app/lib/pages/b_page/history/controller.dart new file mode 100644 index 0000000..3a17016 --- /dev/null +++ b/ln_jq_app/lib/pages/b_page/history/controller.dart @@ -0,0 +1,186 @@ +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 startDate = DateTime.now().subtract(const Duration(days: 7)).obs; + final Rx endDate = DateTime.now().obs; + final TextEditingController plateNumberController = TextEditingController(); + + final RxString totalHydrogen = '0 kg'.obs; + final RxString totalCompletions = '0 次'.obs; + + final RxList historyList = [].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); + + @override + void onInit() { + super.onInit(); + fetchHistoryData(); + } + + Future fetchHistoryData() async { + isLoading.value = true; + try { + var response = await HttpService.to.post( + "appointment/orderAddHyd/sitOrderPage", + data: { + // --- 直接使用 DateFormat 来格式化日期 --- + 'startDate': _apiDateFormat.format(startDate.value), + 'endDate': _apiDateFormat.format(endDate.value), + 'plateNumber': plateNumberController.text, + 'pageNum': 1, + 'pageSize': 50, + 'stationName': "", // 加氢站名称 + }, + ); + + if (response == null || response.data == null) { + showToast('无法获取历史记录'); + _resetData(); + return; + } + + final baseModel = BaseModel.fromJson(response.data); + if (baseModel.code == 0 && baseModel.data != null) { + final dataMap = baseModel.data as Map; + + totalHydrogen.value = '${dataMap['totalHydrogen'] ?? 0} kg'; + totalCompletions.value = '${dataMap['totalCompletions'] ?? 0} 次'; + + final List listFromServer = dataMap['records'] ?? []; + historyList.assignAll( + listFromServer + .map((item) => ReservationModel.fromJson(item as Map)) + .toList(), + ); + hasData.value = historyList.isNotEmpty; + } else { + showToast(baseModel.message); + _resetData(); + } + } catch (e) { + showToast('获取历史记录失败: $e'); + _resetData(); + } finally { + isLoading.value = false; + } + } + + void _resetData() { + totalHydrogen.value = '0 kg'; + totalCompletions.value = '0 次'; + 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(); + } +} diff --git a/ln_jq_app/lib/pages/b_page/history/view.dart b/ln_jq_app/lib/pages/b_page/history/view.dart new file mode 100644 index 0000000..fb3d9c4 --- /dev/null +++ b/ln_jq_app/lib/pages/b_page/history/view.dart @@ -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 { + 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)), + ], + ), + ); + } +} diff --git a/ln_jq_app/lib/pages/b_page/site/view.dart b/ln_jq_app/lib/pages/b_page/site/view.dart index abdc2dc..9012495 100644 --- a/ln_jq_app/lib/pages/b_page/site/view.dart +++ b/ln_jq_app/lib/pages/b_page/site/view.dart @@ -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,48 +115,59 @@ class SitePage extends GetView { 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, - children: [ - Text( - '今日预约信息', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.white, + Expanded( + child: GestureDetector( + onTap: () { + controller.renderData(); + }, + child: Row( + children: [ + Text( + '今日预约信息', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + 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( + ElevatedButton( onPressed: () { - controller.renderData(); + Get.to(() => const HistoryPage()); }, - 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), + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(5), ), + elevation: 2, + ), + child: const Text( + '历史记录', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), ), ],