diff --git a/ln_jq_app/assets/images/history_bg.png b/ln_jq_app/assets/images/history_bg.png new file mode 100644 index 0000000..e06d5e1 Binary files /dev/null and b/ln_jq_app/assets/images/history_bg.png differ diff --git a/ln_jq_app/assets/images/ic_ex_menu@2x.png b/ln_jq_app/assets/images/ic_ex_menu@2x.png new file mode 100644 index 0000000..c63c6b0 Binary files /dev/null and b/ln_jq_app/assets/images/ic_ex_menu@2x.png differ diff --git a/ln_jq_app/assets/images/ic_serch@2x.png b/ln_jq_app/assets/images/ic_serch@2x.png new file mode 100644 index 0000000..cd6ae6a Binary files /dev/null and b/ln_jq_app/assets/images/ic_serch@2x.png differ diff --git a/ln_jq_app/lib/pages/b_page/history/controller.dart b/ln_jq_app/lib/pages/b_page/history/controller.dart deleted file mode 100644 index 63874b8..0000000 --- a/ln_jq_app/lib/pages/b_page/history/controller.dart +++ /dev/null @@ -1,216 +0,0 @@ -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); - String stationName = ""; - - @override - void onInit() { - super.onInit(); - - final args = Get.arguments as Map; - stationName = args['stationName'] as String; - fetchHistoryData(); - } - - Future 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.fromJson(response.data); - final dataMap = baseModel.data as Map; - totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0} kg'; - totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0} 次'; - } catch (e) { - totalHydrogen.value = '0 kg'; - totalCompletions.value = '0 次'; - } - } - - Future 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.fromJson(response.data); - if (baseModel.code == 0 && baseModel.data != null) { - final dataMap = baseModel.data as Map; - - 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() { - 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 index 7832f5c..e472210 100644 --- a/ln_jq_app/lib/pages/b_page/history/view.dart +++ b/ln_jq_app/lib/pages/b_page/history/view.dart @@ -1,107 +1,173 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:ln_jq_app/common/styles/theme.dart'; +import 'package:getx_scaffold/getx_scaffold.dart'; +import 'package:ln_jq_app/common/login_util.dart'; import 'package:ln_jq_app/pages/b_page/history/controller.dart'; -import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel +import 'package:ln_jq_app/pages/b_page/site/controller.dart'; class HistoryPage extends GetView { - const HistoryPage({Key? key}) : super(key: key); + const HistoryPage({super.key}); @override Widget build(BuildContext context) { - Get.put(HistoryController()); + return GetBuilder( + init: HistoryController(), + id: 'history', + builder: (_) { + return Scaffold( + backgroundColor: const Color(0xFFF7F8FA), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20), + onPressed: () => Get.back(), + ), + title: _buildSearchBox(), + ), + body: Column( + children: [ + _buildFilterBar(), + _buildSummaryCard(), + Expanded(child: _buildHistoryList()), + ], + ), + ); + }, + ); + } - 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 _buildSearchBox() { + return Container( + height: 36, + decoration: BoxDecoration( + color: const Color(0xFFF2F3F5), + borderRadius: BorderRadius.circular(18), + ), + child: TextField( + controller: controller.plateNumberController, + onSubmitted: (v) => controller.refreshData(), + decoration: const InputDecoration( + hintText: '搜索车牌号', + hintStyle: TextStyle(color: Color(0xFFBBBBBB), fontSize: 14), + prefixIcon: Icon(Icons.search_sharp, color: Color(0xFFBBBBBB), size: 20), + border: InputBorder.none, + contentPadding: EdgeInsets.only(bottom: 12), ), ), ); } - 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), - ), + Widget _buildFilterBar() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: controller.statusOptions.entries.map((entry) { + return Obx(() { + bool isSelected = controller.selectedStatus.value == entry.key; + return GestureDetector( + onTap: () => controller.onStatusSelected(entry.key), + child: Container( + margin: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF006633) : Colors.white, + borderRadius: BorderRadius.circular(15), + ), + child: Text( + entry.value, + style: TextStyle( + color: isSelected + ? Colors.white + : Color.fromRGBO(148, 163, 184, 1), + fontSize: 12.sp, + fontWeight: isSelected ? FontWeight.bold : FontWeight.w600, + ), + ), + ), + ); + }); + }).toList(), ), ), - 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)), - ), - ), - ], - ), + ), + _buildTimeFilterIcon(), + ], ), ); } + Widget _buildTimeFilterIcon() { + return PopupMenuButton( + icon: LoginUtil.getAssImg("ic_ex_menu@2x"), + onSelected: controller.onDateTypeSelected, + itemBuilder: (context) => [ + const PopupMenuItem(value: 'week', child: Text('最近一周')), + const PopupMenuItem(value: 'month', child: Text('最近一月')), + const PopupMenuItem(value: 'three_month', child: Text('最近三月')), + ], + ); + } + 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, - ), - ], - ), + return Container( + margin: const EdgeInsets.only(left: 16, right: 16,bottom: 12), + padding: const EdgeInsets.all(20), + height: 160, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + image: const DecorationImage( + image: AssetImage('assets/images/history_bg.png'), + fit: BoxFit.cover, ), ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('加氢站', style: TextStyle(color: Colors.white70, fontSize: 12)), + Text( + controller.stationName, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + Obx( + () => Row( + children: [ + _buildSummaryItem('实际加氢量', '${controller.totalHydrogen.value} Kg'), + const SizedBox(width: 40), + _buildSummaryItem('预约完成次数', '${controller.totalCompletions.value} 次'), + ], + ), + ), + ], + ), + ); + } + + Widget _buildSummaryItem(String label, String value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(color: Colors.white60, fontSize: 12)), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], ); } @@ -110,143 +176,138 @@ class HistoryPage extends GetView { if (controller.isLoading.value) { return const Center(child: CircularProgressIndicator()); } - if (!controller.hasData.value) { - return const Center(child: Text('没有找到相关记录')); + if (controller.historyList.isEmpty) { + return const Center( + child: Text('暂无相关记录', style: TextStyle(color: Color(0xFF999999))), + ); } return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), 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), - ), - ); + return _buildHistoryItem(controller.historyList[index]); }, ); }); } - Widget _buildStatusChip(ReservationStatus status) { - String text; - Color color; + Widget _buildHistoryItem(ReservationModel item) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '车牌号', + style: TextStyle( + color: Color.fromRGBO(148, 163, 184, 1), + fontSize: 12.sp, + ), + ), + const SizedBox(height: 4), + Text( + item.plateNumber, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + _buildStatusBadge(item.status), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + _buildInfoColumn('加氢时间:', item.time), + _buildInfoColumn('加氢量', '${item.amount} Kg', isRight: true), + ], + ), + ], + ), + ); + } + + Widget _buildInfoColumn(String label, String value, {bool isRight = false}) { + return Expanded( + child: Column( + crossAxisAlignment: isRight ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(color: Color.fromRGBO(148, 163, 184, 1), fontSize: 12.sp), + ), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + fontSize: isRight ? 16 : 13, + fontWeight: isRight ? FontWeight.bold : FontWeight.normal, + color: const Color(0xFF333333), + ), + ), + ], + ), + ); + } + + Widget _buildStatusBadge(ReservationStatus status) { + String text = '未知'; + Color bgColor = Colors.grey.shade100; + Color textColor = Colors.grey; + switch (status) { case ReservationStatus.pending: text = '待加氢'; - color = Colors.orange; + bgColor = const Color(0xFFFFF7E8); + textColor = const Color(0xFFFF9800); break; case ReservationStatus.completed: text = '已加氢'; - color = Colors.greenAccent; + bgColor = const Color(0xFFE8F5E9); + textColor = const Color(0xFF4CAF50); break; case ReservationStatus.rejected: text = '拒绝加氢'; - color = Colors.red; + bgColor = const Color(0xFFFFEBEE); + textColor = const Color(0xFFF44336); break; case ReservationStatus.unadded: text = '未加氢'; - color = Colors.red; + bgColor = const Color(0xFFFFEBEE); + textColor = const Color(0xFFF44336); break; case ReservationStatus.cancel: text = '已取消'; - color = Colors.red; + bgColor = const Color(0xFFFFEBEE); + textColor = const Color(0xFFF44336); break; default: text = '未知状态'; - color = Colors.grey; + bgColor = Colors.grey; + textColor = Colors.grey; break; } + return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), + color: bgColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: textColor.withOpacity(0.3)), ), - 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)), - ], + child: Text( + text, + style: TextStyle(color: textColor, fontSize: 12, 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 1df6c42..863662e 100644 --- a/ln_jq_app/lib/pages/b_page/site/view.dart +++ b/ln_jq_app/lib/pages/b_page/site/view.dart @@ -78,12 +78,12 @@ class SitePage extends GetView { Column( children: [ _buildSearchView(), + SizedBox(height: 15.h), controller.hasReservationData ? _buildReservationListView() : _buildEmptyReservationView(), ], ), - SizedBox(height: 35.h), //第三部分 Container( @@ -136,7 +136,7 @@ class SitePage extends GetView { ], ), ), - SizedBox(height: 75.h), + SizedBox(height: 105.h), ], ); } @@ -391,8 +391,9 @@ class SitePage extends GetView { /// 构建“有预约数据”的列表视图 Widget _buildReservationListView() { - return ListView.separated( + return ListView.builder( shrinkWrap: true, + padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), // 因为外层已有滚动,这里禁用内部滚动 itemCount: controller.reservationList.length, @@ -401,7 +402,6 @@ class SitePage extends GetView { // 调用新的方法来构建每一项 return _buildReservationItem(index, item); }, - separatorBuilder: (context, index) => const SizedBox(height: 0), // 列表项之间的间距 ); }