Files
ln-ios/ln_jq_app/lib/pages/c_page/reservation/view.dart
2026-01-27 13:57:14 +08:00

714 lines
25 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: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: () {
hideKeyboard();
},
child: Stack(
children: [
Positioned(
left: 0,
right: 0,
bottom: 10.h,
top: 0,
child: SingleChildScrollView(
child: Column(
children: [
_buildUserInfoCard(),
Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16.h),
_buildCarInfoCard(),
SizedBox(height: 32.h),
_buildReservationFormCard(context),
],
),
),
],
),
),
),
Positioned(
left: 20.w,
right: 20.w,
bottom: 90.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());
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
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: Color(0xFFF0F2F5),
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF006633)),
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("剩余氢量", style: TextStyle(fontSize: 10, color: Colors.grey)),
Text("${controller.leftHydrogen}Kg", style: const TextStyle(fontSize: 10, color: Color(0xFF006633), fontWeight: FontWeight.bold)),
],
),
],
),
],
),
),
],
),
),
);
}
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.bold, color: Colors.black87)),
],
);
}
/// 构建预约表单卡片
Widget _buildReservationFormCard(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPickerRow(
label: '日期',
value: controller.formattedDate,
icon: Icons.calendar_today_outlined,
onTap: () => controller.pickDate(context),
),
_buildPickerRow(
label: '预约时间',
value: controller.formattedTimeSlot,
icon: Icons.access_time_outlined,
onTap: () => controller.pickTime(context),
),
_buildTextField(
label: '预约氢量(KG)',
controller: controller.amountController,
hint: '当前最大可预约氢量${controller.difference}(KG)',
keyboardType: TextInputType.number,
),
/*_buildTextField(
label: '车牌号',
controller: controller.plateNumberController,
hint: '请输入车牌号', // 修改提示文案
enabled: false, // 设置为不可编辑
),*/
_buildStationSelector(),
const SizedBox(height: 20),
],
),
),
),
);
}
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, 40.h), // 高度与另一个按钮保持一致
side: const BorderSide(color: Color.fromRGBO(226, 232, 240, 1)),
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
child: const Text(
'查看预约',
style: TextStyle(
color: Color.fromRGBO(119, 119, 119, 1),
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: controller.submitReservation,
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 40.h),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: AppTheme.themeColor,
foregroundColor: Colors.white,
),
child: const Text(
'提交预约',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
),
),
),
],
);
}
// 表单中的可点击行 (用于日期和时间选择)
Widget _buildPickerRow({
required String label,
required String value,
required IconData icon,
required VoidCallback onTap,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 13)),
const SizedBox(height: 8),
InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[400]!),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(value, style: const TextStyle(fontSize: 14)),
Icon(icon, color: Colors.grey, size: 20),
],
),
),
),
],
),
);
}
// 表单中的文本输入框
Widget _buildTextField({
required String label,
required TextEditingController controller,
required String hint,
TextInputType? keyboardType,
bool enabled = true,
}) {
bool showCounter = keyboardType == TextInputType.number;
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 13)),
const SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly, // 只允许数字输入
],
enabled: enabled,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
isDense: true,
hintText: hint,
hintStyle: TextStyle(fontSize: 14),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
filled: !enabled,
fillColor: Colors.grey[100],
// 左侧减号按钮
prefixIcon: showCounter
? IconButton(
icon: const Icon(Icons.remove, color: Colors.blue),
onPressed: () => _updateAmount(-1),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
)
: null,
// 右侧加号按钮
suffixIcon: showCounter
? IconButton(
icon: const Icon(Icons.add, color: Colors.blue),
onPressed: () => _updateAmount(1),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
)
: null,
),
),
],
),
);
}
// :更新氢量逻辑
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),
);
}
Widget _buildStationSelector() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.all(0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('加氢站', style: TextStyle(color: Colors.grey[600], fontSize: 14)),
TextButton(
onPressed: () {
controller.getSiteList();
},
child: const Text('刷新'),
),
],
),
),
Obx(
() => DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
hint: const Text(
'请选择加氢站',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
// items 列表现在从 stationOptions (StationModel列表) 构建
items: controller.stationOptions
.map(
(station) => DropdownMenuItem<String>(
value: station.hydrogenId, // value 是站点的唯一ID
enabled: station.isSelect == 1,
child: _buildDropdownItem(station), // child 是自定义的 Widget
),
)
.toList(),
value:
// 当前的站点 处理默认
controller.selectedStationId.value ??
(controller.stationOptions.isNotEmpty
? controller.stationOptions.first.hydrogenId
: null),
// 当前选中的是站点ID
onChanged: (value) {
if (value != null) {
controller.selectedStationId.value = value;
}
},
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),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[400]!),
),
),
iconStyleData: const IconStyleData(
icon: Icon(Icons.arrow_drop_down, color: Colors.grey),
iconSize: 24,
),
dropdownStyleData: DropdownStyleData(
maxHeight: 300,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
),
menuItemStyleData: const MenuItemStyleData(
height: 60, // 增加下拉项的高度
),
),
),
),
],
);
}
/// 构建下拉菜单中的每一项
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),
],
),
);
}
}