8 Commits
main ... dev

Author SHA1 Message Date
88b16ca69e 今日预约搜索 2025-12-03 16:00:18 +08:00
ba3467810c 新增数据 2025-12-03 15:04:58 +08:00
0bfedd54cb 可编辑预约 2025-12-03 10:39:34 +08:00
f25d7e4567 增加 加氢站可用标记 2025-12-02 10:36:56 +08:00
4cdedff654 默认时间段修改 2025-12-01 17:03:16 +08:00
4f99ab4164 修改选择器 2025-12-01 15:27:00 +08:00
cdc5af7f45 限制 2025-12-01 13:59:02 +08:00
20e5e58ded 增加提交预约限制 2025-12-01 13:56:17 +08:00
12 changed files with 700 additions and 209 deletions

View File

@@ -21,7 +21,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.lingniu.driver" applicationId = "com.lnkj.ln_jq_app"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion

View File

@@ -294,14 +294,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -374,14 +370,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -495,7 +487,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 2228B9MS38; DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -503,7 +495,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp; PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -682,7 +674,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 2228B9MS38; DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -690,7 +682,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp; PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -706,7 +698,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 2228B9MS38; DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -714,7 +706,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp; PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View File

@@ -4,6 +4,7 @@ class StationModel {
final String address; final String address;
final String price; final String price;
final String siteStatusName; // 例如 "维修中" final String siteStatusName; // 例如 "维修中"
final int isSelect; // 新增字段 1是可用 0是不可用
StationModel({ StationModel({
required this.hydrogenId, required this.hydrogenId,
@@ -11,6 +12,7 @@ class StationModel {
required this.address, required this.address,
required this.price, required this.price,
required this.siteStatusName, required this.siteStatusName,
required this.isSelect,
}); });
// 从 JSON map 创建对象的工厂构造函数 // 从 JSON map 创建对象的工厂构造函数
@@ -21,6 +23,7 @@ class StationModel {
address: json['address'] ?? '地址未知', address: json['address'] ?? '地址未知',
price: json['price']?.toString() ?? '0.00', price: json['price']?.toString() ?? '0.00',
siteStatusName: json['siteStatusName'] ?? '', siteStatusName: json['siteStatusName'] ?? '',
isSelect: json['isSelect'] as int? ?? 0, // 新增字段的解析,默认为 0
); );
} }
} }

View File

@@ -7,11 +7,14 @@ class AppTheme {
static const Color themeColor = Color(0xFF0c83c3); static const Color themeColor = Color(0xFF0c83c3);
//http://192.168.110.222:8080/
static const String test_service_url = "http://47.100.49.118:8090/api/"; static const String test_service_url = "https://beta-esg.api.lnh2e.com/";
static const String release_service_url = ""; static const String release_service_url = "";
//加氢站相关查询 //加氢站相关查询
static const String jiaqing_service_url = "https://lnh2e.com/api/lingniu-manager-v1/v1/"; static const String jiaqing_service_url =
"https://beta.lnh2e.com/api/lingniu-manager-v1/v1/";
//车辆信息 //车辆信息
static const String car_service_url = "http://47.99.166.38:20000/"; static const String car_service_url = "http://47.99.166.38:20000/";
@@ -56,10 +59,7 @@ class AppTheme {
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Color.fromARGB(255, 34, 34, 34), backgroundColor: Color.fromARGB(255, 34, 34, 34),
centerTitle: true, centerTitle: true,
titleTextStyle: TextStyle( titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
fontSize: 18,
fontWeight: FontWeight.bold,
),
), ),
bottomAppBarTheme: BottomAppBarThemeData( bottomAppBarTheme: BottomAppBarThemeData(
color: Color.fromARGB(255, 34, 34, 34), color: Color.fromARGB(255, 34, 34, 34),

View File

@@ -14,7 +14,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
WidgetsBinding widgetsBinding = await init( WidgetsBinding widgetsBinding = await init(
isDebug: false, isDebug: true,
logTag: '小羚羚', logTag: '小羚羚',
supportedLocales: [Locale('zh', 'CN')], supportedLocales: [Locale('zh', 'CN')],
); );
@@ -30,7 +30,7 @@ void main() async {
// 设计稿尺寸 单位dp // 设计稿尺寸 单位dp
designSize: const Size(390, 844), designSize: const Size(390, 844),
// Getx Log // Getx Log
enableLog: false, enableLog: true,
// 默认的跳转动画 // 默认的跳转动画
defaultTransition: Transition.rightToLeft, defaultTransition: Transition.rightToLeft,
// 主题模式 // 主题模式

View File

@@ -76,7 +76,8 @@ class ReservationModel {
String startTimeStr = json['startTime']?.toString() ?? ''; String startTimeStr = json['startTime']?.toString() ?? '';
String endTimeStr = json['endTime']?.toString() ?? ''; String endTimeStr = json['endTime']?.toString() ?? '';
String dateStr = json['date']?.toString() ?? ''; String dateStr = json['date']?.toString() ?? '';
String timeRange = (startTimeStr.isNotEmpty && endTimeStr.isNotEmpty && dateStr.isNotEmpty) String timeRange =
(startTimeStr.isNotEmpty && endTimeStr.isNotEmpty && dateStr.isNotEmpty)
? '$dateStr ${startTimeStr.substring(11, 16)}-${endTimeStr.substring(11, 16)}' // 截取 HH:mm ? '$dateStr ${startTimeStr.substring(11, 16)}-${endTimeStr.substring(11, 16)}' // 截取 HH:mm
: '时间未定'; : '时间未定';
@@ -119,6 +120,8 @@ class SiteController extends GetxController with BaseControllerMixin {
List<ReservationModel> reservationList = []; List<ReservationModel> reservationList = [];
Timer? _refreshTimer; Timer? _refreshTimer;
final TextEditingController searchController = TextEditingController();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@@ -130,6 +133,7 @@ class SiteController extends GetxController with BaseControllerMixin {
@override @override
void onClose() { void onClose() {
stopAutoRefresh(); stopAutoRefresh();
searchController.dispose();
super.onClose(); super.onClose();
} }
@@ -155,13 +159,17 @@ class SiteController extends GetxController with BaseControllerMixin {
Future<void> fetchReservationData() async { Future<void> fetchReservationData() async {
showLoading("加载中"); showLoading("加载中");
final String searchText = searchController.text.trim();
try { try {
var response = await HttpService.to.post( var response = await HttpService.to.post(
"appointment/orderAddHyd/sitOrderPage", "appointment/orderAddHyd/sitOrderPage",
data: { data: {
'stationName': name, // 使用从 renderData 中获取到的 name 'stationName': name, // 使用从 renderData 中获取到的 name
'pageNum': 1, 'pageNum': 1,
'pageSize': 30, // 暂时不考虑分页一次获取30条 'pageSize': 50, // 暂时不考虑分页一次获取30条
'plateNumber': searchText, // 加氢站名称
'phone': searchText, //手机号
}, },
); );
@@ -267,6 +275,8 @@ class SiteController extends GetxController with BaseControllerMixin {
String orderAmount = ""; String orderAmount = "";
String completedAmount = ""; String completedAmount = "";
String name = ""; String name = "";
String orderTotalAmount = "";
String orderUnfinishedAmount = "";
Future<void> renderData() async { Future<void> renderData() async {
try { try {
@@ -286,8 +296,14 @@ class SiteController extends GetxController with BaseControllerMixin {
orderAmount = result.data["orderAmount"].toString(); orderAmount = result.data["orderAmount"].toString();
completedAmount = result.data["completedAmount"].toString(); completedAmount = result.data["completedAmount"].toString();
name = result.data["name"].toString(); name = result.data["name"].toString();
orderTotalAmount = result.data["orderTotalAmount"] ?? "";
orderUnfinishedAmount = result.data["orderUnfinishedAmount"] ?? "";
leftHydrogen = leftHydrogen.isEmpty ? "统计中" : leftHydrogen.toString(); leftHydrogen = leftHydrogen.isEmpty ? "统计中" : leftHydrogen.toString();
orderTotalAmount = orderTotalAmount.isEmpty ? "统计中" : orderTotalAmount.toString();
orderUnfinishedAmount = orderUnfinishedAmount.isEmpty
? "统计中"
: orderUnfinishedAmount.toString();
//加载列表数据 //加载列表数据
fetchReservationData(); fetchReservationData();

View File

@@ -94,6 +94,17 @@ class SitePage extends GetView<SiteController> {
], ],
), ),
), ),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(controller.orderTotalAmount, '加氢总量'),
_buildStatItem(controller.orderUnfinishedAmount, '未加氢总量'),
],
),
),
], ],
), ),
), ),
@@ -150,6 +161,7 @@ class SitePage extends GetView<SiteController> {
], ],
), ),
), ),
_buildSearchView(),
controller.hasReservationData controller.hasReservationData
? _buildReservationListView() ? _buildReservationListView()
: _buildEmptyReservationView(), : _buildEmptyReservationView(),
@@ -179,6 +191,66 @@ class SitePage extends GetView<SiteController> {
); );
} }
//搜索输入框,提示可以输入车牌或者手机
Widget _buildSearchView() {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 44,
child: TextField(
controller: controller.searchController, // 绑定控制器
decoration: InputDecoration(
hintText: '输入车牌号或完整手机号查询',
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(22),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(22),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(22),
borderSide: BorderSide(color: Get.theme.primaryColor, width: 1.5),
),
// 清除按钮
suffixIcon: IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
controller.searchController.clear();
controller.fetchReservationData(); // 清除后也刷新一次
},
),
),
onSubmitted: (value) {
// 用户在键盘上点击“完成”或“搜索”时触发
controller.fetchReservationData();
},
),
),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
// 点击“搜索”按钮时触发
FocusScope.of(Get.context!).unfocus(); // 收起键盘
controller.fetchReservationData();
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(8),
),
child: const Icon(Icons.search_rounded),
),
],
),
);
}
/// 构建单个统计项 /// 构建单个统计项
Widget _buildStatItem(String value, String label, {Color valueColor = Colors.blue}) { Widget _buildStatItem(String value, String label, {Color valueColor = Colors.blue}) {
return Expanded( return Expanded(

View File

@@ -9,32 +9,72 @@ import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/station_model.dart'; import 'package:ln_jq_app/common/model/station_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart'; import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; import 'package:ln_jq_app/pages/b_page/site/controller.dart';
import 'package:ln_jq_app/pages/c_page/reservation_edit/controller.dart';
import 'package:ln_jq_app/pages/c_page/reservation_edit/view.dart';
import 'package:ln_jq_app/pages/qr_code/view.dart'; import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import '../../../common/styles/theme.dart'; import '../../../common/styles/theme.dart';
/// Helper class for managing time slots
class TimeSlot {
final TimeOfDay start;
final TimeOfDay end;
TimeSlot(this.start, this.end);
String get display {
final startStr =
'${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}';
final endStr =
'${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}';
return '$startStr - $endStr';
}
}
class C_ReservationController extends GetxController with BaseControllerMixin { class C_ReservationController extends GetxController with BaseControllerMixin {
@override @override
String get builderId => 'reservation'; String get builderId => 'reservation';
final Rx<DateTime> selectedDate = DateTime.now().obs; final DateTime _now = DateTime.now();
final Rx<TimeOfDay> startTime = TimeOfDay.now().obs;
final Rx<TimeOfDay> endTime = TimeOfDay.fromDateTime( // 计算当前时间属于哪个半小时区间
DateTime.now().add(const Duration(minutes: 30)), late final TimeOfDay _initialStartTime = _calculateInitialStartTime(_now);
).obs; late final TimeOfDay _initialEndTime = TimeOfDay.fromDateTime(
_getDateTimeFromTimeOfDay(_initialStartTime).add(const Duration(minutes: 30)),
);
late final Rx<DateTime> selectedDate = DateTime(_now.year, _now.month, _now.day).obs;
late final Rx<TimeOfDay> startTime = _initialStartTime.obs;
late final Rx<TimeOfDay> endTime = _initialEndTime.obs;
/// 静态辅助方法,用于计算初始的开始时间
static TimeOfDay _calculateInitialStartTime(DateTime now) {
if (now.minute < 30) {
// 如果当前分钟小于30则开始时间为当前小时的0分
return TimeOfDay(hour: now.hour, minute: 0);
} else {
// 如果当前分钟大于等于30则开始时间为当前小时的30分
return TimeOfDay(hour: now.hour, minute: 30);
}
}
/// 静态辅助方法将TimeOfDay转换为DateTime
static DateTime _getDateTimeFromTimeOfDay(TimeOfDay time) {
final now = DateTime.now();
return DateTime(now.year, now.month, now.day, time.hour, time.minute);
}
final TextEditingController amountController = TextEditingController(); final TextEditingController amountController = TextEditingController();
TextEditingController plateNumberController = TextEditingController(); TextEditingController plateNumberController = TextEditingController();
final RxList<StationModel> stationOptions = <StationModel>[].obs; final RxList<StationModel> stationOptions = <StationModel>[].obs;
final Rxn<String> selectedStationId = Rxn<String>(); // 用 ID 来选择 final Rxn<String> selectedStationId = Rxn<String>();
String get formattedDate => DateFormat('yyyy-MM-dd').format(selectedDate.value); String get formattedDate => DateFormat('yyyy-MM-dd').format(selectedDate.value);
String get formattedStartTime => _formatTimeOfDay(startTime.value); /// 时间段
String get formattedTimeSlot =>
String get formattedEndTime => _formatTimeOfDay(endTime.value); '${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}';
void pickDate(BuildContext context) { void pickDate(BuildContext context) {
DateTime tempDate = selectedDate.value; DateTime tempDate = selectedDate.value;
@@ -84,22 +124,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
Expanded( Expanded(
child: CupertinoDatePicker( child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date, mode: CupertinoDatePickerMode.date,
initialDateTime: initialDateTime: selectedDate.value,
selectedDate.value.isBefore(
DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
),
)
? DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
)
: selectedDate.value,
// 设置最小可选日期为“今天凌晨0点”
minimumDate: DateTime( minimumDate: DateTime(
DateTime.now().year, DateTime.now().year,
DateTime.now().month, DateTime.now().month,
@@ -118,52 +143,72 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
); );
} }
void pickTime(BuildContext context, bool isStartTime) { ///30 分钟为间隔 时间选择器
// 1. 确定当前操作的时间和初始值 void pickTime(BuildContext context) {
TimeOfDay initialTime = isStartTime ? startTime.value : endTime.value; final now = DateTime.now();
DateTime now = DateTime.now(); final isToday =
selectedDate.value.year == now.year &&
selectedDate.value.month == now.month &&
selectedDate.value.day == now.day;
// 2. 准备小时和分钟的数据源 final List<TimeSlot> availableSlots = [];
List<int> hours = List<int>.generate(24, (index) => index); for (int i = 0; i < 48; i++) {
List<int> minutes = [0, 30]; final startMinutes = i * 30;
final endMinutes = startMinutes + 30;
// 3. 计算初始选中的索引 final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
int initialHour = initialTime.hour; final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
// 将初始分钟校准到0或30并找到对应的索引
int initialMinute = initialTime.minute;
int minuteIndex = initialMinute < 30 ? 0 : 1;
initialMinute = minutes[minuteIndex]; // 校准后的分钟
// 如果校准后导致时间早于当前时间,需要向上调整 final slotStartDateTime = DateTime(
final selectedDay = DateTime(selectedDate.value.year, selectedDate.value.month, selectedDate.value.day); selectedDate.value.year,
final today = DateTime(now.year, now.month, now.day); selectedDate.value.month,
if (selectedDay.isAtSameMomentAs(today)) { selectedDate.value.day,
if (initialHour < now.hour || (initialHour == now.hour && initialMinute < now.minute)) { startTime.hour,
initialHour = now.hour; startTime.minute,
if (now.minute > 30) { );
// 如果当前分钟>30, 则进位到下一小时的0分
initialHour = (now.hour + 1) % 24; // 如果不是今天,所有时间段都有效
initialMinute = 0; if (!isToday) {
availableSlots.add(TimeSlot(startTime, endTime));
} else { } else {
// 否则取30分 // 如果是今天,需要判断该时间段是否可选
initialMinute = 30; // 创建时间段的结束时间对象
final slotEndDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
endTime.hour,
endTime.minute,
);
// 只要时间段的结束时间晚于当前时间,这个时间段就是可预约的
if (slotEndDateTime.isAfter(now)) {
availableSlots.add(TimeSlot(startTime, endTime));
} }
} }
} }
// 重新获取校准后的索引 if (availableSlots.isEmpty) {
minuteIndex = minutes.indexOf(initialMinute); showToast('今天已没有可预约的时间段');
return;
}
int initialItem = availableSlots.indexWhere(
(slot) =>
slot.start.hour == startTime.value.hour &&
(startTime.value.minute < 30
? slot.start.minute == 0
: slot.start.minute == 30),
);
if (initialItem == -1) {
initialItem = 0;
}
// 4. 创建 FixedExtentScrollController 来控制滚轮的初始位置 TimeSlot tempSlot = availableSlots[initialItem];
final FixedExtentScrollController hourController =
FixedExtentScrollController(initialItem: hours.indexOf(initialHour));
final FixedExtentScrollController minuteController =
FixedExtentScrollController(initialItem: minuteIndex);
// 5. 存储临时选择的值 final FixedExtentScrollController scrollController = FixedExtentScrollController(
int tempHour = initialHour; initialItem: initialItem,
int tempMinute = initialMinute; );
Get.bottomSheet( Get.bottomSheet(
Container( Container(
@@ -184,94 +229,41 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
children: [ children: [
CupertinoButton( CupertinoButton(
onPressed: () => Get.back(), onPressed: () => Get.back(),
child: const Text('取消', style: TextStyle(color: CupertinoColors.systemGrey)), child: const Text(
'取消',
style: TextStyle(color: CupertinoColors.systemGrey),
),
), ),
CupertinoButton( CupertinoButton(
onPressed: () { onPressed: () {
final pickedTempTime = TimeOfDay(hour: tempHour, minute: tempMinute); startTime.value = tempSlot.start;
final now = DateTime.now(); endTime.value = tempSlot.end;
// --- 合并和简化校验逻辑 ---
final selectedDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
pickedTempTime.hour,
pickedTempTime.minute,
);
// 验证1: 不能选择过去的时间(留出一分钟缓冲)
if (selectedDateTime.isBefore(now.subtract(const Duration(minutes: 1)))) {
showToast('不能选择过去的时间');
return;
}
// 验证2: 结束时间必须晚于开始时间
if (isStartTime) {
startTime.value = pickedTempTime;
final startInMinutes = pickedTempTime.hour * 60 + pickedTempTime.minute;
final endInMinutes = endTime.value.hour * 60 + endTime.value.minute;
// 如果新的开始时间大于等于结束时间,自动将结束时间设置为开始时间+30分钟
if (startInMinutes >= endInMinutes) {
final newEndDateTime = selectedDateTime.add(const Duration(minutes: 30));
endTime.value = TimeOfDay.fromDateTime(newEndDateTime);
}
} else { // 正在设置结束时间
final startInMinutes = startTime.value.hour * 60 + startTime.value.minute;
final endInMinutes = pickedTempTime.hour * 60 + pickedTempTime.minute;
if (endInMinutes <= startInMinutes) {
showToast('结束时间必须晚于开始时间');
return;
}
endTime.value = pickedTempTime;
}
Get.back(); Get.back();
}, },
child: const Text( child: const Text(
'确认', '确认',
style: TextStyle(color: AppTheme.themeColor, fontWeight: FontWeight.bold), style: TextStyle(
color: AppTheme.themeColor,
fontWeight: FontWeight.bold,
),
), ),
), ),
], ],
), ),
), ),
const Divider(height: 1, color: Color(0xFFE5E5E5)), const Divider(height: 1, color: Color(0xFFE5E5E5)),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 小时选择器
Expanded( Expanded(
child: CupertinoPicker( child: CupertinoPicker(
scrollController: hourController, scrollController: scrollController,
itemExtent: 32.0, itemExtent: 40.0,
onSelectedItemChanged: (index) { onSelectedItemChanged: (index) {
tempHour = hours[index]; tempSlot = availableSlots[index];
}, },
children: hours children: availableSlots
.map((h) => Center(child: Text(h.toString().padLeft(2, '0')))) .map((slot) => Center(child: Text(slot.display)))
.toList(), .toList(),
), ),
), ),
// 分钟选择器
Expanded(
child: CupertinoPicker(
scrollController: minuteController,
itemExtent: 32.0,
onSelectedItemChanged: (index) {
tempMinute = minutes[index];
},
children: minutes
.map((m) => Center(child: Text(m.toString().padLeft(2, '0'))))
.toList(),
),
),
],
),
),
], ],
), ),
), ),
@@ -279,8 +271,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
); );
} }
// 用于存储上一次成功预约的信息 // 用于存储上一次成功预约的信息
ReservationModel? lastSuccessfulReservation; ReservationModel? lastSuccessfulReservation;
@@ -311,7 +301,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
} }
final dateStr = formattedDate; final dateStr = formattedDate;
final startTimeStr = '$dateStr ${formattedStartTime}:00'; final startTimeStr =
'$dateStr ${_formatTimeOfDay(startTime.value)}:00'; // Use helper directly
if (lastSuccessfulReservation != null && if (lastSuccessfulReservation != null &&
lastSuccessfulReservation!.id == selectedStationId.value && lastSuccessfulReservation!.id == selectedStationId.value &&
lastSuccessfulReservation!.startTime == startTimeStr) { lastSuccessfulReservation!.startTime == startTimeStr) {
@@ -319,29 +311,36 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
return; return;
} }
// 将选择的日期和时间组合成一个完整的 DateTime 对象 final reservationEndDateTime = DateTime(
final reservationStartDateTime = DateTime(
selectedDate.value.year, selectedDate.value.year,
selectedDate.value.month, selectedDate.value.month,
selectedDate.value.day, selectedDate.value.day,
startTime.value.hour, endTime.value.hour,
startTime.value.minute, endTime.value.minute,
); );
// 检查预约时间是否当前时间之前 //判断预约区间的结束时间是否早于当前时间留出1分钟缓冲
if (reservationStartDateTime.isBefore(DateTime.now())) { if (reservationEndDateTime.isBefore(
showToast("不可预约过去的时间"); DateTime.now().subtract(const Duration(minutes: 1)),
)) {
showToast("无法预约已过去的时间段");
return; return;
} }
try { try {
showLoading("提交中");
final selectedStation = stationOptions.firstWhere( final selectedStation = stationOptions.firstWhere(
(s) => s.hydrogenId == selectedStationId.value, (s) => s.hydrogenId == selectedStationId.value,
); );
final dateStr = formattedDate; // "yyyy-MM-dd" if (selectedStation.siteStatusName != "营运中") {
final startTimeStr = '$dateStr ${formattedStartTime}:00'; // "yyyy-MM-dd HH:mm:ss" showToast("该站点${selectedStation.siteStatusName},暂无法预约");
final endTimeStr = '$dateStr ${formattedEndTime}:00'; return;
}
showLoading("提交中");
final endTimeStr =
'$dateStr ${_formatTimeOfDay(endTime.value)}:00'; // Use helper directly
var responseData = await HttpService.to.post( var responseData = await HttpService.to.post(
'appointment/orderAddHyd/saveOrUpdate', 'appointment/orderAddHyd/saveOrUpdate',
@@ -369,7 +368,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
if (result.code == 0) { if (result.code == 0) {
showSuccessToast("预约成功"); showSuccessToast("预约成功");
// 预约成功后,保存当前预约信息
lastSuccessfulReservation = ReservationModel( lastSuccessfulReservation = ReservationModel(
id: selectedStationId.value!, id: selectedStationId.value!,
hydAmount: ampuntStr, hydAmount: ampuntStr,
@@ -537,14 +535,37 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
), // Blue border ), // Blue border
), ),
child: Text( child: Text(
reservation.stateName, reservation.stateName +
"-" +
reservation.addStatusName,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF1890FF), color: Color(0xFF1890FF),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
Container( plateNumber.isEmpty
? SizedBox()
: GestureDetector(
onTap: () async {
var result = await Get.to(
() => ReservationEditPage(),
arguments: {
'reservation': reservation,
'difference': difference,
},
binding: BindingsBuilder(() {
Get.put(ReservationEditController());
}),
preventDuplicates: false,
);
showToast("555$result");
if (result == true) {
Get.back();
getReservationList();
}
},
child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
vertical: 4, vertical: 4,
@@ -556,11 +577,12 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
reservation.addStatusName, "修改",
style: const TextStyle( style: const TextStyle(
color: Color(0xFFFA8C16), color: Color(0xFFFA8C16),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12, fontSize: 14,
),
), ),
), ),
), ),

View File

@@ -258,16 +258,10 @@ class ReservationPage extends GetView<C_ReservationController> {
onTap: () => controller.pickDate(context), onTap: () => controller.pickDate(context),
), ),
_buildPickerRow( _buildPickerRow(
label: '开始时间', label: '预约时间',
value: controller.formattedStartTime, value: controller.formattedTimeSlot,
icon: Icons.access_time_outlined, icon: Icons.access_time_outlined,
onTap: () => controller.pickTime(context, true), onTap: () => controller.pickTime(context),
),
_buildPickerRow(
label: '结束时间',
value: controller.formattedEndTime,
icon: Icons.access_time_outlined,
onTap: () => controller.pickTime(context, false),
), ),
_buildTextField( _buildTextField(
label: '预约氢量(KG)', label: '预约氢量(KG)',
@@ -438,6 +432,7 @@ class ReservationPage extends GetView<C_ReservationController> {
.map( .map(
(station) => DropdownMenuItem<String>( (station) => DropdownMenuItem<String>(
value: station.hydrogenId, // value 是站点的唯一ID value: station.hydrogenId, // value 是站点的唯一ID
enabled: station.isSelect == 1,
child: _buildDropdownItem(station), // child 是自定义的 Widget child: _buildDropdownItem(station), // child 是自定义的 Widget
), ),
) )
@@ -484,7 +479,10 @@ class ReservationPage extends GetView<C_ReservationController> {
/// 构建下拉菜单中的每一项 /// 构建下拉菜单中的每一项
Widget _buildDropdownItem(StationModel station) { Widget _buildDropdownItem(StationModel station) {
bool isMaintenance = (station.siteStatusName != '营运中'); bool isSelectable = (station.isSelect == 1);
//营运并且可用
bool isMaintenance = ((station.siteStatusName != '营运中') && isSelectable);
final textColor = isSelectable ? Colors.black : Colors.grey;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row( child: Row(
@@ -501,14 +499,22 @@ class ReservationPage extends GetView<C_ReservationController> {
Flexible( Flexible(
child: Text( child: Text(
station.name, station.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), style: TextStyle(
color: textColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
), ),
), ),
Text( Text(
' | ¥${station.price}/kg', ' | ¥${station.price}/kg',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), style: TextStyle(
color: textColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
), ),
if (isMaintenance) const SizedBox(width: 8), if (isMaintenance) const SizedBox(width: 8),
if (isMaintenance) if (isMaintenance)

View File

@@ -0,0 +1,253 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:intl/intl.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // For ReservationModel
import 'package:ln_jq_app/common/styles/theme.dart';
class TimeSlot {
final TimeOfDay start;
final TimeOfDay end;
TimeSlot(this.start, this.end);
String get display {
final startStr =
'${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}';
final endStr =
'${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}';
return '$startStr - $endStr';
}
}
class ReservationEditController extends GetxController with BaseControllerMixin {
late final ReservationModel reservation;
late final String difference;
// --- State Variables ---
final Rx<DateTime> selectedDate = DateTime.now().obs;
final Rx<TimeOfDay> startTime = TimeOfDay.now().obs;
final Rx<TimeOfDay> endTime = TimeOfDay.now().obs;
final TextEditingController amountController = TextEditingController();
// --- Getters for UI display ---
String get formattedTimeSlot =>
'${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}';
@override
void onInit() {
super.onInit();
// Expect a Map containing both the reservation and the difference
final args = Get.arguments as Map<String, dynamic>;
reservation = args['reservation'] as ReservationModel;
difference = args['difference'] as String? ?? '0';
try {
// Initialize the UI with the data from the passed reservation object
selectedDate.value = DateFormat('yyyy-MM-dd').parse(reservation.date);
final startDateTime = DateFormat(
'yyyy-MM-dd HH:mm:ss',
).parse(reservation.startTime);
startTime.value = TimeOfDay.fromDateTime(startDateTime);
final endDateTime = DateFormat('yyyy-MM-dd HH:mm:ss').parse(reservation.endTime);
endTime.value = TimeOfDay.fromDateTime(endDateTime);
amountController.text = reservation.hydAmount.replaceAll('kg', '');
} catch (e) {
showErrorToast("加载预约数据时出错");
Get.back(); // Go back if data is invalid
}
}
@override
void onClose() {
amountController.dispose();
super.onClose();
}
/// Reusable time slot picker logic, copied from the reservation creation page.
void pickTime(BuildContext context) {
final now = DateTime.now();
final isToday =
selectedDate.value.year == now.year &&
selectedDate.value.month == now.month &&
selectedDate.value.day == now.day;
final List<TimeSlot> availableSlots = [];
for (int i = 0; i < 48; i++) {
final startMinutes = i * 30;
final endMinutes = startMinutes + 30;
final slotStartTime = TimeOfDay(
hour: startMinutes ~/ 60,
minute: startMinutes % 60,
);
final slotEndTime = TimeOfDay(
hour: (endMinutes ~/ 60) % 24,
minute: endMinutes % 60,
);
final slotStartDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
slotStartTime.hour,
slotStartTime.minute,
);
if (!isToday || slotStartDateTime.isAfter(now)) {
availableSlots.add(TimeSlot(slotStartTime, slotEndTime));
}
}
if (availableSlots.isEmpty) {
showToast('今天已没有可预约的时间段');
return;
}
int initialItem = availableSlots.indexWhere(
(slot) =>
slot.start.hour == startTime.value.hour &&
(startTime.value.minute < 30
? slot.start.minute == 0
: slot.start.minute == 30),
);
if (initialItem == -1) initialItem = 0;
TimeSlot tempSlot = availableSlots[initialItem];
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: [
CupertinoButton(
onPressed: () {
if (Get.isBottomSheetOpen ?? false) {
Navigator.of(Get.overlayContext!).pop(); // 更加安全的关闭当前弹窗的方式
}
},
child: const Text(
'取消',
style: TextStyle(color: CupertinoColors.systemGrey),
),
),
CupertinoButton(
onPressed: () {
startTime.value = tempSlot.start;
endTime.value = tempSlot.end;
if (Get.isBottomSheetOpen ?? false) {
Navigator.of(Get.overlayContext!).pop(); // 更加安全的关闭当前弹窗的方式
}
},
child: const Text(
'确认',
style: TextStyle(
color: AppTheme.themeColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const Divider(height: 1, color: Color(0xFFE5E5E5)),
Expanded(
child: CupertinoPicker(
scrollController: FixedExtentScrollController(initialItem: initialItem),
itemExtent: 40.0,
onSelectedItemChanged: (index) {
tempSlot = availableSlots[index];
},
children: availableSlots
.map((slot) => Center(child: Text(slot.display)))
.toList(),
),
),
],
),
),
backgroundColor: Colors.transparent,
);
}
/// This function will be called when the 'Save Changes' button is pressed.
void updateReservation() async {
String amountStr = amountController.text.toString();
if (amountStr.isEmpty) {
showToast("请输入需要预约的氢量");
return;
}
double amountDouble = (double.tryParse(amountStr) ?? 0.0);
if (amountDouble <= 0) {
showToast("预约氢量必须大于0");
return;
}
if (amountDouble > (double.tryParse(difference) ?? 0.0)) {
showToast('当前最大可预约氢量为$difference(KG)');
return;
}
showLoading("正在保存修改...");
final dateStr = DateFormat('yyyy-MM-dd').format(selectedDate.value);
final startTimeStr = '$dateStr ${_formatTimeOfDay(startTime.value)}:00';
final endTimeStr = '$dateStr ${_formatTimeOfDay(endTime.value)}:00';
try {
var responseData = await HttpService.to.post(
'appointment/orderAddHyd/saveOrUpdate',
data: {
'id': reservation.id,
'startTime': startTimeStr,
'endTime': endTimeStr,
'hydAmount': amountStr,
},
);
if (responseData == null || responseData.data == null) {
showToast('服务暂不可用,请稍后');
dismissLoading();
return;
}
var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) {
showSuccessToast("修改成功");
//弹窗刷新数据
Get.back(result: true);
} else {
showErrorToast(result.message);
}
} catch (e) {
showErrorToast("保存失败,请稍后再试");
} finally {
dismissLoading();
}
}
String _formatTimeOfDay(TimeOfDay time) {
final hour = time.hour.toString().padLeft(2, '0');
final minute = time.minute.toString().padLeft(2, '0');
return '$hour:$minute';
}
@override
String get builderId => "reservationeditpage";
}

View File

@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';
class ReservationEditPage extends GetView<ReservationEditController> {
const ReservationEditPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetBuilder(
init: ReservationEditController(),
id: 'reservationeditpage',
builder: (_) {
return Scaffold(
appBar: AppBar(title: const Text('修改预约'), centerTitle: true),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: 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.formattedTimeSlot,
icon: Icons.access_time_outlined,
onTap: () => controller.pickTime(context),
),
_buildTextField(
label: '预约氢量(KG)',
controller: controller.amountController,
// Use Obx to make the hint text responsive if needed, though here it's static.
hint: '当前最大可预约氢量${controller.difference}(KG)',
keyboardType: TextInputType.number,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: controller.updateReservation,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: const Text(
'保存修改',
style: TextStyle(fontSize: 16, 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: 14)),
const SizedBox(height: 8),
InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
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: 16)),
Icon(icon, color: Colors.grey, size: 20),
],
),
),
),
],
),
);
}
Widget _buildTextField({
required String label,
required TextEditingController controller,
required String hint,
TextInputType? keyboardType,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
const SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
),
),
],
),
);
}
}

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.1.0+1 version: 1.0.0+1
environment: environment:
sdk: ^3.9.0 sdk: ^3.9.0