import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:ln_jq_app/common/login_util.dart'; import 'package:ln_jq_app/common/model/station_model.dart'; import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/pages/c_page/message/view.dart'; import 'package:ln_jq_app/pages/qr_code/view.dart'; import 'package:ln_jq_app/storage_service.dart'; import 'controller.dart'; import 'reservation_list_bottomsheet.dart'; ///加氢预约 class ReservationPage extends GetView { ReservationPage({super.key}); @override Widget build(BuildContext context) { return GetBuilder( init: C_ReservationController(), id: 'reservation', builder: (_) { return Scaffold( backgroundColor: Color.fromRGBO(247, 249, 251, 1), body: GestureDetector( onTap: () => unfocus(), child: Stack( children: [ Positioned.fill( child: SingleChildScrollView( child: Column( children: [ _buildUserInfoCard(), Padding( padding: EdgeInsets.symmetric(horizontal: 18.w), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox(height: 16.h), _buildCarInfoCard(), SizedBox(height: 24.h), _buildReservationFormCard(context), SizedBox(height: 180.h), ], ), ), ], ), ), ), Positioned( left: 20.w, right: 20.w, bottom: 110.h, child: _buildReservationItem(context), ), ], ), ), ); }, ); } /// 构建用户信息卡片 Widget _buildUserInfoCard() { return Card( elevation: 1, color: Colors.white, margin: EdgeInsets.zero, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), ), ), child: Column( children: [ Padding( padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 50), child: Row( children: [ Stack( children: [ CircleAvatar( radius: 25, backgroundColor: Colors.white, child: LoginUtil.getAssImg('ic_user_logo@2x'), ), Positioned( right: 0, bottom: 0, child: SizedBox( height: 16.h, width: 16.w, child: LoginUtil.getAssImg('ic_logo@2x'), ), ), ], ), SizedBox(width: 8.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( "${StorageService.to.name}", style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), ), SizedBox(width: 8.w), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: const Color.fromRGBO(236, 255, 234, 1), border: Border.all(color: const Color(0xFFB7E19F)), borderRadius: BorderRadius.circular(12), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)), SizedBox(width: 4), Text( "绿色先锋", style: TextStyle( color: Color(0xFF52C41A), fontSize: 10, ), ), ], ), ), ], ), const SizedBox(height: 4), Text( "羚牛ID:${StorageService.to.phone}", style: const TextStyle(color: Colors.grey, fontSize: 11), ), ], ), ), IconButton( onPressed: () => Get.to(() => const MessagePage()), icon: Badge( smallSize: 8, backgroundColor: controller.isNotice ? Colors.red : Colors.transparent, child: const Icon( Icons.notifications_outlined, color: Colors.black87, size: 30, ), ), ), ], ), ), Padding( padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildModernStatItem('累计加氢量', '', controller.fillingWeight, ''), const SizedBox(width: 8), _buildModernStatItem('总加氢次数', '', controller.fillingTimes, ''), const SizedBox(width: 8), _buildModernStatItem('今日里程', '', "7kg", ''), ], ), ), ], ), ); } Widget _buildModernStatItem(String title, String subtitle, String value, String unit) { return Expanded( child: Container( padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( color: const Color(0xFFF8F9FA), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87, ), ), Text(subtitle, style: const TextStyle(fontSize: 9, color: Colors.grey)), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( value, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87, ), ), Text(unit, style: const TextStyle(fontSize: 10, color: Colors.black54)), ], ), ], ), ), ); } Widget _buildCarInfoCard() { return Card( elevation: 2, color: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ Expanded(flex: 4, child: LoginUtil.getAssImg('ic_car_bg@2x')), const SizedBox(width: 16), Expanded( flex: 6, child: Column( children: [ _buildCarDataItem('剩余电量', '36.8%'), const SizedBox(height: 8), _buildCarDataItem('剩余氢量', '${controller.leftHydrogen}Kg'), const SizedBox(height: 8), _buildCarDataItem('百公里氢耗', '${controller.workEfficiency}Kg'), const SizedBox(height: 12), Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: controller.progressValue, minHeight: 6, backgroundColor: const Color(0xFFF0F2F5), valueColor: const AlwaysStoppedAnimation( Color(0xFF006633), ), ), ), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( "剩余氢量", style: TextStyle( fontSize: 10, color: Colors.grey, fontWeight: FontWeight.w400, ), ), Text( "${controller.leftHydrogen}Kg", style: const TextStyle( fontSize: 10, color: Color(0xFF006633), fontWeight: FontWeight.w600, ), ), ], ), ], ), ], ), ), ], ), ), ); } Widget _buildCarDataItem(String label, String value) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), Text( value, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ], ); } /// --- 构建预约表单卡片--- Widget _buildReservationFormCard(BuildContext context) { return Column( children: [ // 1. 顶部:日期与时间选择 Card( elevation: 0, color: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "预约日期与时间", style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87, ), ), const SizedBox(height: 20), _buildHorizontalDateSelector(), const SizedBox(height: 32), _buildTimeSlider(context), ], ), ), ), const SizedBox(height: 12), // 2. 底部:氢量与站点 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: _buildAmountSliderSection()), const SizedBox(width: 12), Expanded(child: _buildStationCardSection(context)), ], ), ], ); } /// 水平日期选择器 Widget _buildHorizontalDateSelector() { final DateTime today = DateTime( DateTime.now().year, DateTime.now().month, DateTime.now().day, ); final DateTime tomorrow = today.add(const Duration(days: 1)); final List dates = List.generate( 5, (index) => today.add(Duration(days: index)), ); const List weekMap = ['日', '一', '二', '三', '四', '五', '六']; return SizedBox( height: 75, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: dates.length, itemBuilder: (context, index) { DateTime date = dates[index]; bool isSelectable = date.isAtSameMomentAs(today) || date.isAtSameMomentAs(tomorrow); bool isSelected = controller.selectedDate.value.year == date.year && controller.selectedDate.value.month == date.month && controller.selectedDate.value.day == date.day; return GestureDetector( onTap: isSelectable ? () { controller.selectedDate.value = date; controller.resetTimeForSelectedDate(); controller.updateUi(); } : null, child: Container( width: 58, margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( color: isSelected ? const Color(0xFF006633) : const Color(0xFFF8F9FA), borderRadius: BorderRadius.circular(14), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( weekMap[date.weekday % 7], style: TextStyle( fontSize: 12, color: isSelected ? Colors.white70 : (isSelectable ? Colors.grey : Colors.grey[300]), ), ), const SizedBox(height: 6), Text( "${date.day}", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: isSelected ? Colors.white : (isSelectable ? Colors.black87 : Colors.grey[300]), ), ), ], ), ), ); }, ), ); } /// 时间 Slider 选择器 Widget _buildTimeSlider(BuildContext context) { return Obx(() { // 这里的逻辑对应 Controller 中的 24 小时可用 Slot int currentIdx = controller.startTime.value.hour; return Column( children: [ Stack( alignment: Alignment.topCenter, clipBehavior: Clip.none, children: [ Padding( padding: const EdgeInsets.only(bottom: 12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), decoration: BoxDecoration( color: const Color(0xFFE6F4EA), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white, width: 2), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Text( controller.formattedTimeSlot, style: const TextStyle( color: Color(0xFF006633), fontSize: 13, fontWeight: FontWeight.bold, ), ), ), ), ], ), SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: 10, activeTrackColor: const Color(0xFF006633), inactiveTrackColor: const Color(0xFFF0F2F5), thumbColor: Colors.white, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 12, elevation: 4, ), overlayColor: const Color(0xFF006633).withOpacity(0.1), ), child: Slider( value: currentIdx.toDouble(), min: 0, max: 23, divisions: 23, onChanged: (val) { int hour = val.toInt(); // 模拟 Controller 中的 pickTime 逻辑校验 final now = DateTime.now(); final isToday = controller.selectedDate.value.year == now.year && controller.selectedDate.value.month == now.month && controller.selectedDate.value.day == now.day; if (isToday && hour < now.hour) { // 如果是今天且小时数小于当前,则忽略 return; } controller.startTime.value = TimeOfDay(hour: hour, minute: 0); controller.endTime.value = TimeOfDay(hour: (hour + 1) % 24, minute: 0); }, ), ), ], ); }); } /// 氢量滑块区域 Widget _buildAmountSliderSection() { return Card( elevation: 0, color: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(13.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "预约氢量", style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, ), ), const Text( "Refuel Amount", style: TextStyle(fontSize: 10, color: Colors.grey), ), const SizedBox(height: 24), Column( children: [ SliderTheme( data: SliderTheme.of(Get.context!).copyWith( trackHeight: 6, activeTrackColor: const Color(0xFF006633), inactiveTrackColor: const Color(0xFFF0F2F5), thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 9), ), child: Slider( value: controller.current > controller.maxVal ? controller.maxVal : controller.current, min: 0, max: controller.maxVal, onChanged: (val) { final safeVal = val < 1 ? 1 : val; // 最小 1 controller.amountController.text = safeVal.toStringAsFixed(0); controller.renderSliderTheme(); }, ), ), const SizedBox(height: 12), Container( height: 40.h, decoration: BoxDecoration( color: const Color(0xFFF8F9FA), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( onPressed: () => _updateAmount(-1), icon: const Icon(Icons.remove, size: 18, color: Colors.grey), ), Text( "${controller.amountController.text} Kg", style: const TextStyle(fontSize: 14, color: Colors.black87), ), IconButton( onPressed: () => _updateAmount(1), icon: const Icon(Icons.add, size: 18, color: Colors.grey), ), ], ), ), ], ), ], ), ), ); } /// 站点卡片区域 Widget _buildStationCardSection(BuildContext context) { return Obx(() { return DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, /// 当前选中值 value: controller.selectedStationId.value, hint: const Text('请选择加氢站', style: TextStyle(fontSize: 14, color: Colors.grey)), /// 下拉数据 items: controller.stationOptions .map( (station) => DropdownMenuItem( value: station.hydrogenId, enabled: station.isSelect == 1, child: _buildDropdownItem(station), ), ) .toList(), /// 选中回调 onChanged: (value) { if (value != null) { controller.selectedStationId.value = value; } }, ///作为 Dropdown 的触发按钮 customButton: _buildStationCard(), /// 隐藏按钮自身样式 buttonStyleData: const ButtonStyleData(padding: EdgeInsets.zero), /// 隐藏默认箭头 iconStyleData: const IconStyleData(icon: SizedBox.shrink()), /// 下拉样式 dropdownStyleData: DropdownStyleData( maxHeight: 300, width: MediaQuery.of(context).size.width / 1.3, decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), ), /// 下拉项高度 menuItemStyleData: const MenuItemStyleData(height: 60), ), ); }); } Widget _buildStationCard() { final stationId = controller.selectedStationId.value; final station = controller.stationOptions.firstWhereOrNull( (s) => s.hydrogenId == stationId, ); return Card( elevation: 0, color: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(13.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "加氢站", style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, ), ), Text("Station", style: TextStyle(fontSize: 10, color: Colors.grey)), ], ), Icon(Icons.more_vert, color: Colors.grey[300], size: 20), ], ), const SizedBox(height: 24), Container( width: double.infinity, height: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), image: const DecorationImage( image: AssetImage("assets/images/bg_map@2x.png"), fit: BoxFit.cover, ), ), child: Stack( children: [ Center( child: Icon( Icons.location_on_outlined, color: Colors.grey[300], size: 40, ), ), Align( alignment: Alignment.bottomCenter, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16), ), ), child: Text( "${station?.name ?? '请选择站点'} | ${station?.price ?? '0.00'}/Kg", textAlign: TextAlign.center, style: const TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ), ], ), ), ], ), ), ); } void _updateAmount(int change) { // 获取当前输入框的值,默认为 0 double currentAmount = double.tryParse(controller.amountController.text) ?? 0; // 获取最大值,注意处理 difference 可能为空或非数字的情况 double maxAmount = double.tryParse(controller.difference.toString()) ?? 9999; // 计算新值 double newAmount = currentAmount + change; // 边界检查 if (newAmount < 1) { newAmount = 1; // 最小不能小于 1 } else if (newAmount > maxAmount) { newAmount = maxAmount; // 最大不能超过 difference } // 如果是整数,去掉小数点显示 if (newAmount == newAmount.toInt()) { controller.amountController.text = newAmount.toInt().toString(); } else { controller.amountController.text = newAmount.toStringAsFixed(2); } // 移动光标到末尾,防止光标跳到前面 controller.amountController.selection = TextSelection.fromPosition( TextPosition(offset: controller.amountController.text.length), ); controller.updateUi(); } Widget _buildReservationItem(BuildContext context) { return Row( children: [ Expanded( flex: 1, child: OutlinedButton( onPressed: () => controller.getReservationList(showPopup: true, addStatus: ''), style: OutlinedButton.styleFrom( minimumSize: Size(double.infinity, 50.h), side: const BorderSide(color: Color.fromRGBO(226, 232, 240, 1)), backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), ), child: const Text( '查看预约', style: TextStyle( color: Color.fromRGBO(119, 119, 119, 1), fontSize: 14, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(width: 16), Expanded( flex: 2, child: ElevatedButton( onPressed: controller.submitReservation, style: ElevatedButton.styleFrom( minimumSize: Size(double.infinity, 50.h), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), backgroundColor: const Color(0xFF006633), foregroundColor: Colors.white, elevation: 4, ), child: const Text( '提交预约', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), ), ), ), ], ); } /// 构建下拉菜单中的每一项 Widget _buildDropdownItem(StationModel station) { bool isSelectable = (station.isSelect == 1); //营运并且可用 bool isMaintenance = ((station.siteStatusName != '营运中') && isSelectable); final textColor = isSelectable ? Colors.black : Colors.grey; return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start, children: [ Flexible( child: Text( station.name, style: TextStyle( color: textColor, fontSize: 14, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), Text( ' | ¥${station.price}/kg', style: TextStyle( color: textColor, fontSize: 14, fontWeight: FontWeight.w500, ), ), if (isMaintenance) const SizedBox(width: 8), if (isMaintenance) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.orange[100], borderRadius: BorderRadius.circular(4), ), child: Text( station.siteStatusName, style: TextStyle( fontSize: 10, color: Colors.orange, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 4), Text( '${station.address}', style: const TextStyle(fontSize: 12, color: Colors.grey), overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ); } Widget _buildSelectedStationButton(StationModel station) { return Container( // 选中后样式 padding: const EdgeInsets.symmetric(horizontal: 12.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[400]!), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded(child: _buildDropdownItem(station)), const Icon(Icons.arrow_drop_down, color: Colors.grey, size: 24), ], ), ); } }