Files
ln-ios/ln_jq_app/lib/pages/c_page/reservation/view.dart
2026-01-29 17:01:21 +08:00

917 lines
32 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<C_ReservationController> {
ReservationPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<C_ReservationController>(
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: () async{
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
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>(
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<DateTime> dates = List.generate(
5,
(index) => today.add(Duration(days: index)),
);
const List<String> 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: 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: Icon(Icons.remove, size: 14.sp, color: Colors.grey),
),
Text(
"${controller.amountController.text}Kg",
style: TextStyle(
fontSize: 11.sp,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
IconButton(
onPressed: () => _updateAmount(1),
icon: Icon(Icons.add, size: 14.sp, color: Colors.grey),
),
],
),
),
],
),
],
),
),
);
}
/// 站点卡片区域
Widget _buildStationCardSection(BuildContext context) {
return Obx(() {
return DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
/// 当前选中值
value: controller.selectedStationId.value,
hint: const Text('请选择加氢站', style: TextStyle(fontSize: 14, color: Colors.grey)),
/// 下拉数据
items: controller.stationOptions
.map(
(station) => DropdownMenuItem<String>(
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.current = newAmount;
// 移动光标到末尾,防止光标跳到前面
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),
],
),
);
}
}