11 Commits

Author SHA1 Message Date
dce9718320 显示周边加氢站 2026-02-11 09:35:42 +08:00
4491aa9b91 ui调整 2026-02-10 16:35:02 +08:00
5364612a6f 更新样式 2026-02-10 13:37:24 +08:00
10867178fa 筛选框样式 2026-02-10 13:35:22 +08:00
a5e2a89e4f 预约列表样式 2026-02-10 11:51:47 +08:00
26c5f9d67a 消息样式修改 2026-02-09 17:57:00 +08:00
9cd87b0535 规则和历史 2026-02-09 17:28:12 +08:00
45e45d8160 积分兑换 2026-02-09 15:10:00 +08:00
87e890f97e 积分兑换首页 2026-02-06 17:37:43 +08:00
dcf925b8c1 商场页 2026-02-06 15:13:33 +08:00
c45863eda6 增加商城页面 2026-02-06 15:11:12 +08:00
28 changed files with 1969 additions and 505 deletions

View File

@@ -25,7 +25,7 @@
display: none !important;
}
/* 去除高德默认的 label 边框背景 */
/* 去除高德默认的 label 边框 and 背景 */
.amap-marker-label {
border: none !important;
background-color: transparent !important;
@@ -221,6 +221,7 @@
var currentLat, currentLng;
var isTruckMode = false;
var isInitialLocationSet = false;
var stationMarkers = []; // 存储所有站点的标记
function initMap() {
@@ -336,6 +337,8 @@
fetchStationInfo(addressComponent.province, addressComponent.city,
addressComponent.district, lat, lng);
fetchStationInfoList(lat, lng);
// 策略1: 优先使用最近的、类型合适的POI的名称
if (pois && pois.length > 0) {
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI这类POI通常是具体的建筑或地点名
@@ -397,7 +400,6 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
// "asoco-token": "e28eada8-4611-4dc2-a942-0122e52f52da"
},
body: JSON.stringify({
province: province,
@@ -437,6 +439,73 @@
.catch(err => console.error('JS->:获取站点信息失败:', err));
}
/**
* 获取站点列表
*/
function fetchStationInfoList(lat, lng) {
fetch('https://beta-esg.api.lnh2e.com/appointment/station/getNearbyHydrogenStationsByLocation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
longitude: lng,
latitude: lat,
})
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误: ' + response.status);
}
return response.json(); // 解析 JSON
})
.then(res => {
console.log("JS->:2 接口完整返回:", JSON.stringify(res));
if (res.code === 0 && res.data && Array.isArray(res.data)) {
// 1. 清除旧的站点标记
stationMarkers.forEach(m => m.setMap(null));
stationMarkers = [];
// 2. 循环标记所有加氢站
res.data.forEach(station => {
var stationIcon = new AMap.Icon({
size: new AMap.Size(32, 32),
image: 'ic_tag.png',
imageSize: new AMap.Size(32, 32)
});
var sMarker = new AMap.Marker({
map: map,
position: [station.longitude, station.latitude],
icon: stationIcon,
offset: new AMap.Pixel(-16, -32),
title: station.name,
label: {
content: '<div class="custom-bubble">' + station.name + '</div>',
direction: 'top'
}
});
// 3. 绑定点击事件:选中即为目的地,并开始规划
sMarker.on('click', function() {
document.getElementById('endInput').value = station.address || station.name;
// 更新当前的 destMarker (如果需要)
if (destMarker) destMarker.setMap(null);
destMarker = sMarker;
startRouteSearch();
});
stationMarkers.push(sMarker);
});
} else {
console.log("JS->: 业务报错或无数据:", res.message);
}
})
.catch(err => console.error('JS->:获取站点信息失败:', err));
}
/**
* 地理编码并在地图标记终点
*/
@@ -447,7 +516,6 @@
if (destMarker) destMarker.setMap(null);
// 2. 创建自定义图标
// 假设图标大小为 32x32你可以根据实际图片尺寸调整 Size
var destIcon = new AMap.Icon({
size: new AMap.Size(32, 32), // 图标尺寸
image: 'ic_tag.png', // 本地图片路径
@@ -459,8 +527,6 @@
map: map,
position: [longitude, latitude],
icon: destIcon, // 使用自定义图标
// 偏移量如果图标底部中心是尖角offset 设为宽的一半的负数,高度的负数
// 这样能确保图片的底部尖端指向地图上的精确位置
offset: new AMap.Pixel(-16, -32),
title: name,
label: {
@@ -469,17 +535,7 @@
}
});
// 4. 打印调试信息
console.log("JS->: 终点标记已添加", address, loc.toString());
// 5. 自动调整视野包含起点和终点
// if (marker) {
// // 如果起点标志已存在,缩放地图以展示两者
// map.setFitView([marker, destMarker], false, [60, 60, 60, 60]);
// } else {
// // 如果没有起点,直接跳到终点
// map.setCenter(loc);
// }
console.log("JS->: 终点标记已添加", address);
}
/**
@@ -523,40 +579,34 @@
document.getElementById('startInput').blur();
document.getElementById('endInput').blur();
// --- 构造路径规划的点 (使用数组方式,更灵活) ---
// --- 构造路径规划的点 ---
var points = [];
// 1. 处理起点逻辑
// 如果输入框是空的,或者写着 "我的位置",则使用 GPS 坐标
if (!startKw || startKw === '我的位置') {
if (!startKw || startKw === '我的位置' || startKw.includes('当前位置')) {
if (!currentLng || !currentLat) {
// 如果还没获取到定位
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler('requestLocation');
}
alert("正在获取定位,请稍后...");
return;
}
// 使用精准坐标对象 (避免高德去猜 '我的位置' 关键词)
points.push({
keyword: '我的位置', // 用于显示的名字
location: new AMap.LngLat(currentLng, currentLat) // 实际导航用的坐标
keyword: '我的位置',
location: new AMap.LngLat(currentLng, currentLat)
});
} else {
// 如果用户手动输入了地点 (例如 "北京南站")
// 直接存入关键词,让高德自己去搜
points.push({
keyword: startKw
});
}
// 2. 处理终点逻辑 (通常是关键词)
// 2. 处理终点逻辑
points.push({
keyword: endKw
});
// 3. 发起搜索
// points 数组里现在是一个起点对象和一个终点对象
driving.search(points, function (status, result) {
if (status === 'complete') {
console.log('JS: 规划成功');
@@ -591,4 +641,4 @@
</script>
</body>
</html>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,10 +1,14 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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'; // Reuse ReservationModel
class HistoryController extends GetxController {
class HistoryController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'history';
// --- 定义 API 需要的日期格式化器 ---
final DateFormat _apiDateFormat = DateFormat('yyyy-MM-dd');
@@ -13,24 +17,39 @@ class HistoryController extends GetxController {
final Rx<DateTime> endDate = DateTime.now().obs;
final TextEditingController plateNumberController = TextEditingController();
final RxString totalHydrogen = '0 kg'.obs;
final RxString totalCompletions = '0'.obs;
final RxString totalHydrogen = '0'.obs;
final RxString totalCompletions = '0'.obs;
final RxList<ReservationModel> historyList = <ReservationModel>[].obs;
final RxBool isLoading = true.obs;
final RxBool hasData = false.obs;
String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value);
String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value);
String stationName = "";
final Map<String, String> statusOptions = {
'': '全部',
'0': '待加氢',
'1': '已加氢',
'2': '未加氢',
'5': '拒绝加氢',
};
final RxString selectedStatus = ''.obs;
final RxString selectedDateType = ''.obs; // week, month, three_month
@override
void onInit() {
super.onInit();
final args = Get.arguments as Map<String, dynamic>;
stationName = args['stationName'] as String;
stationName = args['stationName'] as String? ?? "";
refreshData();
}
void refreshData() {
getAllOrderCounts();
fetchHistoryData();
}
@@ -38,51 +57,50 @@ class HistoryController extends GetxController {
var response = await HttpService.to.post(
"appointment/orderAddHyd/getAllOrderCounts",
data: {
// --- 直接使用 DateFormat 来格式化日期 ---
'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),
/*'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),*/
'plateNumber': plateNumberController.text,
'stationName': stationName, // 加氢站名称
'stationName': stationName,
"status": selectedStatus.value,
"dateType": selectedDateType.value,
},
);
if (response == null || response.data == null) {
totalHydrogen.value = '0 kg';
totalCompletions.value = '0';
totalHydrogen.value = '0';
totalCompletions.value = '0';
return;
}
try {
final baseModel = BaseModel<dynamic>.fromJson(response.data);
final dataMap = baseModel.data as Map<String, dynamic>;
totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0} kg';
totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0}';
totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0}';
totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0}';
} catch (e) {
totalHydrogen.value = '0 kg';
totalCompletions.value = '0';
totalHydrogen.value = '0';
totalCompletions.value = '0';
}
}
Future<void> fetchHistoryData() async {
isLoading.value = true;
//获取数据
getAllOrderCounts();
updateUi();
try {
var response = await HttpService.to.post(
"appointment/orderAddHyd/sitOrderPage",
data: {
// --- 直接使用 DateFormat 来格式化日期 ---
'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),
/*'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),*/
'plateNumber': plateNumberController.text,
'pageNum': 1,
'pageSize': 50,
'stationName': stationName, // 加氢站名称
'stationName': stationName,
"status": selectedStatus.value,
"dateType": selectedDateType.value,
},
);
if (response == null || response.data == null) {
showToast('无法获取历史记录');
_resetData();
return;
}
@@ -90,7 +108,6 @@ class HistoryController extends GetxController {
final baseModel = BaseModel<dynamic>.fromJson(response.data);
if (baseModel.code == 0 && baseModel.data != null) {
final dataMap = baseModel.data as Map<String, dynamic>;
final List<dynamic> listFromServer = dataMap['records'] ?? [];
historyList.assignAll(
listFromServer
@@ -99,14 +116,13 @@ class HistoryController extends GetxController {
);
hasData.value = historyList.isNotEmpty;
} else {
showToast(baseModel.message);
_resetData();
}
} catch (e) {
showToast('获取历史记录失败: $e');
_resetData();
} finally {
isLoading.value = false;
updateUi();
}
}
@@ -115,97 +131,15 @@ class HistoryController extends GetxController {
hasData.value = false;
}
void pickDate(BuildContext context, bool isStartDate) {
// 确定当前操作的日期和临时存储变量
final DateTime initialDate = isStartDate ? startDate.value : endDate.value;
DateTime tempDate = initialDate;
void onStatusSelected(String status) {
if (selectedStatus.value == status) return;
selectedStatus.value = status;
refreshData();
}
// 定义全局的最早可选日期
final DateTime globalMinimumDate = DateTime(2025, 12, 1);
// 动态计算当前选择器的最小/最大日期范围
DateTime minimumDate;
DateTime? maximumDate; // 声明为可空,因为两个日期都可能没有最大限制
if (isStartDate) {
// 当选择【开始日期】时 它的最小日期就是全局最小日期
minimumDate = globalMinimumDate;
// 最大日期没有限制
maximumDate = null;
} else {
// 当选择【结束日期】时 它的最小日期不能早于当前的开始日期
minimumDate = startDate.value;
// 确认结束日期没有最大限制 ---
//最大日期没有限制
maximumDate = null;
}
Get.bottomSheet(
Container(
height: 300,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
// 顶部的取消和确认按钮
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消', style: TextStyle(color: Colors.grey)),
),
TextButton(
onPressed: () {
// 4. 确认后,更新对应的日期变量
if (isStartDate) {
startDate.value = tempDate;
// 如果新的开始日期晚于结束日期,自动将结束日期调整为同一天
if (tempDate.isAfter(endDate.value)) {
endDate.value = tempDate;
}
} else {
endDate.value = tempDate;
}
Get.back();
// 选择日期后自动刷新数据
fetchHistoryData();
},
child: const Text(
'确认',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
const Divider(height: 1),
// 日期选择器
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: initialDate,
// 应用动态计算好的最小/最大日期
minimumDate: minimumDate,
maximumDate: maximumDate,
onDateTimeChanged: (DateTime newDate) {
tempDate = newDate;
},
),
),
],
),
),
backgroundColor: Colors.transparent, // 使底部工作表外的区域透明
);
void onDateTypeSelected(String type) {
selectedDateType.value = type;
refreshData();
}
@override

View File

@@ -1,107 +1,173 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/b_page/history/controller.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
import 'package:ln_jq_app/pages/b_page/site/controller.dart';
class HistoryPage extends GetView<HistoryController> {
const HistoryPage({Key? key}) : super(key: key);
const HistoryPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(HistoryController());
return GetBuilder<HistoryController>(
init: HistoryController(),
id: 'history',
builder: (_) {
return Scaffold(
backgroundColor: const Color(0xFFF7F8FA),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20),
onPressed: () => Get.back(),
),
title: _buildSearchBox(),
),
body: Column(
children: [
_buildFilterBar(),
_buildSummaryCard(),
Expanded(child: _buildHistoryList()),
],
),
);
},
);
}
return Scaffold(
appBar: AppBar(title: const Text('历史记录'), centerTitle: true),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
_buildFilterCard(context),
const SizedBox(height: 12),
_buildSummaryCard(),
const SizedBox(height: 12),
_buildListHeader(),
Expanded(child: _buildHistoryList()),
],
Widget _buildSearchBox() {
return Container(
height: 36,
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(18),
),
child: TextField(
controller: controller.plateNumberController,
onSubmitted: (v) => controller.refreshData(),
decoration: const InputDecoration(
hintText: '搜索车牌号',
hintStyle: TextStyle(color: Color(0xFFBBBBBB), fontSize: 14),
prefixIcon: Icon(Icons.search_sharp, color: Color(0xFFBBBBBB), size: 20),
border: InputBorder.none,
contentPadding: EdgeInsets.only(bottom: 12),
),
),
);
}
Widget _buildFilterCard(BuildContext context) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('时间范围', style: TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 8),
Row(
children: [
Expanded(child: _buildDateField(context, true)),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text(''),
),
Expanded(child: _buildDateField(context, false)),
],
),
const SizedBox(height: 16),
const Text('车牌号', style: TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 8),
SizedBox(
height: 44,
child: TextField(
controller: controller.plateNumberController,
decoration: InputDecoration(
hintText: '请输入车牌号',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
),
Widget _buildFilterBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: controller.statusOptions.entries.map((entry) {
return Obx(() {
bool isSelected = controller.selectedStatus.value == entry.key;
return GestureDetector(
onTap: () => controller.onStatusSelected(entry.key),
child: Container(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF006633) : Colors.white,
borderRadius: BorderRadius.circular(15),
),
child: Text(
entry.value,
style: TextStyle(
color: isSelected
? Colors.white
: Color.fromRGBO(148, 163, 184, 1),
fontSize: 12.sp,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w600,
),
),
),
);
});
}).toList(),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
FocusScope.of(context).unfocus(); // Hide keyboard
controller.fetchHistoryData();
},
icon: const Icon(Icons.search, size: 20),
label: const Text('查询'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 44),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
],
),
),
_buildTimeFilterIcon(),
],
),
);
}
Widget _buildTimeFilterIcon() {
return PopupMenuButton<String>(
icon: LoginUtil.getAssImg("ic_ex_menu@2x"),
onSelected: controller.onDateTypeSelected,
itemBuilder: (context) => [
const PopupMenuItem(value: 'week', child: Text('最近一周')),
const PopupMenuItem(value: 'month', child: Text('最近一月')),
const PopupMenuItem(value: 'three_month', child: Text('最近三月')),
],
);
}
Widget _buildSummaryCard() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Obx(
() => Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildSummaryItem('实际加氢总量', controller.totalHydrogen.value, Colors.blue),
const SizedBox(width: 1, height: 40, child: VerticalDivider()),
_buildSummaryItem(
'预约完成次数',
controller.totalCompletions.value,
Colors.green,
),
],
),
return Container(
margin: const EdgeInsets.only(left: 16, right: 16,bottom: 12),
padding: const EdgeInsets.all(20),
height: 160,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
image: const DecorationImage(
image: AssetImage('assets/images/history_bg.png'),
fit: BoxFit.cover,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('加氢站', style: TextStyle(color: Colors.white70, fontSize: 12)),
Text(
controller.stationName,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Obx(
() => Row(
children: [
_buildSummaryItem('实际加氢量', '${controller.totalHydrogen.value} Kg'),
const SizedBox(width: 40),
_buildSummaryItem('预约完成次数', '${controller.totalCompletions.value}'),
],
),
),
],
),
);
}
Widget _buildSummaryItem(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(color: Colors.white60, fontSize: 12)),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
);
}
@@ -110,143 +176,138 @@ class HistoryPage extends GetView<HistoryController> {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (!controller.hasData.value) {
return const Center(child: Text('没有找到相关记录'));
if (controller.historyList.isEmpty) {
return const Center(
child: Text('暂无相关记录', style: TextStyle(color: Color(0xFF999999))),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: controller.historyList.length,
itemBuilder: (context, index) {
final ReservationModel item = controller.historyList[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text('车牌号: ${item.plateNumber}'),
subtitle: Text.rich(
TextSpan(
children: [
TextSpan(
text: '加氢站: ${item.stationName}\n',
style: TextStyle(fontSize: 16),
),
TextSpan(
text: '时间: ${item.time}\n',
style: TextStyle(fontSize: 16),
),
TextSpan(
text: '加氢量:',
),
TextSpan(
text: '${item.amount}',
style: TextStyle(fontSize: 16, color: AppTheme.themeColor),
),
],
),
)
,
trailing:
// 状态标签
_buildStatusChip(item.status),
),
);
return _buildHistoryItem(controller.historyList[index]);
},
);
});
}
Widget _buildStatusChip(ReservationStatus status) {
String text;
Color color;
Widget _buildHistoryItem(ReservationModel item) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'车牌号',
style: TextStyle(
color: Color.fromRGBO(148, 163, 184, 1),
fontSize: 12.sp,
),
),
const SizedBox(height: 4),
Text(
item.plateNumber,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
_buildStatusBadge(item.status),
],
),
const SizedBox(height: 16),
Row(
children: [
_buildInfoColumn('加氢时间:', item.time),
_buildInfoColumn('加氢量', '${item.amount} Kg', isRight: true),
],
),
],
),
);
}
Widget _buildInfoColumn(String label, String value, {bool isRight = false}) {
return Expanded(
child: Column(
crossAxisAlignment: isRight ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(color: Color.fromRGBO(148, 163, 184, 1), fontSize: 12.sp),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: isRight ? 16 : 13,
fontWeight: isRight ? FontWeight.bold : FontWeight.normal,
color: const Color(0xFF333333),
),
),
],
),
);
}
Widget _buildStatusBadge(ReservationStatus status) {
String text = '未知';
Color bgColor = Colors.grey.shade100;
Color textColor = Colors.grey;
switch (status) {
case ReservationStatus.pending:
text = '待加氢';
color = Colors.orange;
bgColor = const Color(0xFFFFF7E8);
textColor = const Color(0xFFFF9800);
break;
case ReservationStatus.completed:
text = '已加氢';
color = Colors.greenAccent;
bgColor = const Color(0xFFE8F5E9);
textColor = const Color(0xFF4CAF50);
break;
case ReservationStatus.rejected:
text = '拒绝加氢';
color = Colors.red;
bgColor = const Color(0xFFFFEBEE);
textColor = const Color(0xFFF44336);
break;
case ReservationStatus.unadded:
text = '未加氢';
color = Colors.red;
bgColor = const Color(0xFFFFEBEE);
textColor = const Color(0xFFF44336);
break;
case ReservationStatus.cancel:
text = '已取消';
color = Colors.red;
bgColor = const Color(0xFFFFEBEE);
textColor = const Color(0xFFF44336);
break;
default:
text = '未知状态';
color = Colors.grey;
bgColor = Colors.grey;
textColor = Colors.grey;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
color: bgColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: textColor.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, color: color, size: 8),
const SizedBox(width: 4),
Text(
text,
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
),
],
),
);
}
Widget _buildDateField(BuildContext context, bool isStart) {
return Obx(
() => InkWell(
onTap: () => controller.pickDate(context, isStart),
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(isStart ? controller.formattedStartDate : controller.formattedEndDate),
const Icon(Icons.calendar_today, size: 18, color: Colors.grey),
],
),
),
),
);
}
Widget _buildSummaryItem(String label, String value, Color color) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(color: color, fontSize: 22, fontWeight: FontWeight.bold),
),
],
);
}
Widget _buildListHeader() {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 14.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('加氢明细', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
child: Text(
text,
style: TextStyle(color: textColor, fontSize: 12, fontWeight: FontWeight.bold),
),
);
}

View File

@@ -36,7 +36,7 @@ class ReservationPage extends GetView<ReservationController> {
_buildSystemTips(),
SizedBox(height: 24),
_buildLogoutButton(),
SizedBox(height: 75.h),
SizedBox(height: 95.h),
],
),
),

View File

@@ -78,12 +78,12 @@ class SitePage extends GetView<SiteController> {
Column(
children: [
_buildSearchView(),
SizedBox(height: 15.h),
controller.hasReservationData
? _buildReservationListView()
: _buildEmptyReservationView(),
],
),
SizedBox(height: 35.h),
//第三部分
Container(
@@ -136,7 +136,7 @@ class SitePage extends GetView<SiteController> {
],
),
),
SizedBox(height: 75.h),
SizedBox(height: 105.h),
],
);
}
@@ -391,8 +391,9 @@ class SitePage extends GetView<SiteController> {
/// 构建“有预约数据”的列表视图
Widget _buildReservationListView() {
return ListView.separated(
return ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
// 因为外层已有滚动,这里禁用内部滚动
itemCount: controller.reservationList.length,
@@ -401,7 +402,6 @@ class SitePage extends GetView<SiteController> {
// 调用新的方法来构建每一项
return _buildReservationItem(index, item);
},
separatorBuilder: (context, index) => const SizedBox(height: 0), // 列表项之间的间距
);
}

View File

@@ -3,6 +3,7 @@ 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/pages/c_page/car_info/view.dart';
import 'package:ln_jq_app/pages/c_page/mall/mall_view.dart';
import 'package:ln_jq_app/pages/c_page/map/view.dart';
import 'package:ln_jq_app/pages/c_page/mine/view.dart';
import 'package:ln_jq_app/pages/c_page/reservation/view.dart';
@@ -33,7 +34,7 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
}
List<Widget> _buildPages() {
return [ReservationPage(), MapPage(), CarInfoPage(), MinePage()];
return [ReservationPage(), MapPage(), MallPage(), CarInfoPage(), MinePage()];
}
// 自定义导航栏 (悬浮胶囊样式)
@@ -58,8 +59,9 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
children: [
_buildNavItem(0, "ic_h2_select@2x", "ic_h2@2x"),
_buildNavItem(1, "ic_map_select@2x", "ic_map@2x"),
_buildNavItem(2, "ic_car_select@2x", "ic_car@2x"),
_buildNavItem(3, "ic_user_select@2x", "ic_user@2x"),
_buildNavItem(2, "ic_mall_select@2x", "ic_mall@2x"),
_buildNavItem(3, "ic_car_select@2x", "ic_car@2x"),
_buildNavItem(4, "ic_user_select@2x", "ic_user@2x"),
],
),
),
@@ -82,7 +84,8 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
child: SizedBox(
height: 24,
width: 24,
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),),
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),
),
),
);
}

View File

@@ -0,0 +1,113 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/c_page/mall/exchange_success/view.dart';
import '../mall_controller.dart';
class MallDetailController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'mall_detail';
late final int goodsId;
final Rx<GoodsModel?> goodsDetail = Rx<GoodsModel?>(null);
final addressController = TextEditingController();
final nameController = TextEditingController();
final phoneController = TextEditingController();
final formKey = GlobalKey<FormState>();
@override
void onInit() {
super.onInit();
goodsId = Get.arguments['goodsId'] as int;
getGoodsDetail();
}
@override
bool get listenLifecycleEvent => true;
Future<void> getGoodsDetail() async {
try {
var response = await HttpService.to.post(
'appointment/score/getScoreGoodsDetail',
data: {'goodsId': goodsId},
);
if (response != null && response.data != null) {
var result = BaseModel<GoodsModel>.fromJson(
response.data,
dataBuilder: (dataJson) => GoodsModel.fromJson(dataJson),
);
if (result.code == 0 && result.data != null) {
goodsDetail.value = result.data;
} else {
showErrorToast('加载失败: ${result.message}');
Get.back();
}
}
} catch (e) {
log('获取商品详情失败: $e');
showErrorToast('网络异常,请稍后重试');
Get.back();
} finally {
updateUi();
}
}
/// 兑换商品
void exchange() async {
if (!formKey.currentState!.validate()) {
return;
}
/*
final mallController = Get.find<MallController>();
if (mallController.userScore.value < (goodsDetail.value?.score ?? 0)) {
showWarningToast('积分不足');
return;
}*/
// 接口调用预留
showLoading('兑换中...');
final goods = goodsDetail.value;
if (goods == null) {
showErrorToast('兑换失败,请稍后重试');
return;
}
try {
var response = await HttpService.to.post(
'appointment/score/scoreExchange',
data: {
"goodsId": goods.id,
"address": addressController.text,
"name": nameController.text,
"phone": phoneController.text,
},
);
if (response != null && response.data != null) {
var result = BaseModel.fromJson(response.data);
if (result.code == 0) {
Get.off(() => MallExchangeSuccessPage());
}
}
} catch (e) {
log('兑换失败: $e');
showErrorToast('兑换失败,请稍后重试');
} finally {
dismissLoading();
}
}
@override
void onClose() {
addressController.dispose();
nameController.dispose();
phoneController.dispose();
super.onClose();
}
}

View File

@@ -0,0 +1,281 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'controller.dart';
class MallDetailPage extends GetView<MallDetailController> {
const MallDetailPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MallDetailController>(
init: MallDetailController(),
id: 'mall_detail',
builder: (_) {
return Scaffold(
backgroundColor: const Color(0xFFF7F8FA),
appBar: AppBar(
title: const Text('商品兑换'),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 20),
onPressed: () => Get.back(),
),
),
body: GestureDetector(
onTap: () {
hideKeyboard();
},
child: _buildBody(),
),
bottomNavigationBar: _buildBottomButton(),
);
},
);
}
Widget _buildBody() {
final goods = controller.goodsDetail.value;
if (goods == null) return const Center(child: Text('商品信息不存在'));
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: controller.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildGoodsInfoCard(goods),
const SizedBox(height: 24),
_buildSectionTitle('填写收货信息'),
const SizedBox(height: 16),
_buildInputLabel('详细地址'),
_buildTextField(
controller: controller.addressController,
hint: '请输入完整的收货地址',
icon: Icons.location_on_outlined,
),
const SizedBox(height: 16),
_buildInputLabel('收货人姓名'),
_buildTextField(
controller: controller.nameController,
hint: '请输入收货人姓名',
icon: Icons.person_outline,
),
const SizedBox(height: 16),
_buildInputLabel('联系电话'),
_buildTextField(
controller: controller.phoneController,
hint: '请输入手机号码',
icon: Icons.phone_android_outlined,
keyboardType: TextInputType.phone,
),
const SizedBox(height: 40),
Center(
child: Text(
'兑换成功后商品会在3个工作日内邮寄\n请注意查收',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF999999),
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
}
Widget _buildGoodsInfoCard(goods) {
return Container(
padding: const EdgeInsets.all(17),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: goods.goodsImage != null
? Image.network(
goods.goodsImage!,
width: 94.w,
height: 94.h,
fit: BoxFit.cover,
)
: Container(
width: 80,
height: 80,
color: Colors.grey[200],
child: const Icon(Icons.image, color: Colors.grey),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
goods.goodsName,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
SizedBox(height: 8.h),
Row(
children: [
Text(
'${goods.score}',
style: TextStyle(
fontSize: 20.sp,
color: Color(0xFF4CAF50),
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
Text(
'积分',
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
),
),
],
),
SizedBox(height: 10.h),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'数量: 1',
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.w500,
color: Color(0xFF666666),
),
),
),
],
),
),
],
),
);
}
Widget _buildSectionTitle(String title) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 6.w,
height: 16.h,
decoration: BoxDecoration(
color: const Color(0xFF4CAF50),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Color.fromRGBO(148, 163, 184, 1),
),
),
],
);
}
Widget _buildInputLabel(String label) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w600,
color: Color.fromRGBO(100, 116, 139, 1),
),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String hint,
required IconData icon,
TextInputType? keyboardType,
}) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
textAlign: TextAlign.start,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(
color: Color.fromRGBO(134, 144, 156, 1),
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
prefixIcon: Icon(icon, color: const Color(0xFF999999), size: 20),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '内容不能为空';
}
return null;
},
),
);
}
Widget _buildBottomButton() {
return Container(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 30),
child: ElevatedButton(
onPressed: controller.exchange,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF007A45),
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
elevation: 0,
),
child: const Text(
'兑换商品',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/common/index.dart';
import 'package:ln_jq_app/common/login_util.dart';
class MallExchangeSuccessPage extends StatelessWidget {
const MallExchangeSuccessPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 20, color: Colors.black),
onPressed: () => Get.back(), // 返回首页
),
title: const Text('商品兑换', style: TextStyle(color: Colors.black, fontSize: 18)),
),
body: Center(
child: Column(
children: [
SizedBox(height: 114.h),
_buildSuccessIcon(),
const SizedBox(height: 24),
Text(
'兑换成功',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
const SizedBox(height: 8),
Text(
'预计 3 日内发货\n请留意查收',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14.sp, color: Color(0xFF999999)),
),
const SizedBox(height: 60),
ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF007A45),
minimumSize: const Size(140, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
),
child: Text(
'返回首页',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w400,
fontSize: 16.sp,
),
),
),
],
),
),
);
}
Widget _buildSuccessIcon() {
return Container(child: LoginUtil.getAssImg("mall_pay_success@2x"));
}
}

View File

@@ -0,0 +1,160 @@
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/c_page/mall/detail/view.dart';
import 'package:ln_jq_app/pages/c_page/mall/orders/view.dart';
import 'package:ln_jq_app/pages/c_page/mall/rule/view.dart';
class GoodsModel {
final int id;
final String categoryId;
final String goodsName;
final String? goodsImage;
final int originalScore;
final int score;
final int stock;
final int status;
GoodsModel({
required this.id,
required this.categoryId,
required this.goodsName,
this.goodsImage,
required this.originalScore,
required this.score,
required this.stock,
required this.status,
});
factory GoodsModel.fromJson(Map<String, dynamic> json) {
return GoodsModel(
id: json['id'] as int,
categoryId: json['categoryId']?.toString() ?? '',
goodsName: json['goodsName']?.toString() ?? '',
goodsImage: json['goodsImage'],
originalScore: json['originalScore'] as int? ?? 0,
score: json['score'] as int? ?? 0,
stock: json['stock'] as int? ?? 0,
status: json['status'] as int? ?? 1,
);
}
}
class UserScore {
final int score;
final int todaySign;
UserScore({required this.score, required this.todaySign});
factory UserScore.fromJson(Map<String, dynamic> json) {
return UserScore(
score: json['score'] as int? ?? 0,
todaySign: json['todaySign'] as int? ?? 1,
);
}
}
class MallController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'mall';
final RxInt userScore = 0.obs;
final RxInt todaySign = 1.obs; // 0可签到1已签到
final RxList<GoodsModel> goodsList = <GoodsModel>[].obs;
final RxBool isLoading = true.obs;
@override
void onInit() {
super.onInit();
refreshData();
}
Future<void> refreshData() async {
isLoading.value = true;
await Future.wait([getUserScore(), getGoodsList()]);
isLoading.value = false;
updateUi();
}
/// 获取用户积分和签到状态
Future<void> getUserScore() async {
try {
var response = await HttpService.to.post('appointment/score/getUserScore');
if (response != null && response.data != null) {
var result = BaseModel<UserScore>.fromJson(
response.data,
dataBuilder: (dataJson) => UserScore.fromJson(dataJson),
);
if (result.code == 0 && result.data != null) {
userScore.value = result.data!.score;
todaySign.value = result.data!.todaySign;
}
}
} catch (e) {
log('获取积分失败: $e');
}
}
/// 签到逻辑
Future<void> signAction() async {
if (todaySign.value == 1) return;
showLoading('签到中...');
try {
var response = await HttpService.to.post('appointment/score/sign');
dismissLoading();
if (response != null && response.data != null) {
var result = BaseModel.fromJson(response.data);
if (result.code == 0) {
showSuccessToast('签到成功');
getUserScore(); // 签到成功后刷新积分
} else {
showErrorToast(result.message);
}
}
} catch (e) {
dismissLoading();
showErrorToast('签到异常');
}
}
/// 获取商品列表
Future<void> getGoodsList() async {
try {
var response = await HttpService.to.post(
'appointment/score/getScoreGoodsList',
data: {'categoryId': 0},
);
if (response != null && response.data != null) {
var result = BaseModel<List<GoodsModel>>.fromJson(
response.data,
dataBuilder: (dataJson) {
var list = dataJson as List;
return list.map((e) => GoodsModel.fromJson(e)).toList();
},
);
if (result.code == 0 && result.data != null) {
goodsList.assignAll(result.data!);
}
}
} catch (e) {
log('获取商品列表失败: $e');
}
}
/// 兑换商品
void exchangeGoods(GoodsModel goods) {
Get.to(() => const MallDetailPage(), arguments: {'goodsId': goods.id})
?.then((_) => refreshData());
}
///规则说明
void toRuleDes() {
Get.to(() => const MallRulePage());
}
///历史订单
void toOrders() {
Get.to(() => const MallOrdersPage());
}
}

View File

@@ -0,0 +1,345 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'mall_controller.dart';
class MallPage extends GetView<MallController> {
const MallPage({super.key});
@override
Widget build(BuildContext context) {
return GetX<MallController>(
init: MallController(),
builder: (_) {
return Scaffold(
backgroundColor: Color.fromRGBO(247, 249, 251, 1),
body: RefreshIndicator(
onRefresh: controller.refreshData,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column(children: [_buildAppBar(), _buildScoreCard()]),
),
),
_buildSectionHeader(),
_buildGoodsGrid(),
SliverToBoxAdapter(child: SizedBox(height: 120.h)),
],
),
),
);
},
);
}
Widget _buildAppBar() {
return Container(
padding: EdgeInsets.only(top: MediaQuery.of(Get.context!).padding.top, bottom: 0),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFF4CAF50),
borderRadius: BorderRadius.circular(8),
),
child: LoginUtil.getAssImg("mall_bar@2x"),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'氢能商城',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
Row(
children: [
const Text(
'Hydrogen Energy Mall',
style: TextStyle(fontSize: 12, color: Color(0xFF999999)),
),
],
),
],
),
),
/*IconButton(
onPressed: () {},
icon: const Icon(Icons.notifications_none, color: Color(0xFF333333)),
),*/
],
),
);
}
Widget _buildScoreCard() {
return Container(
margin: EdgeInsets.only(top: 22.h),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color.fromRGBO(2, 27, 31, 1), Color.fromRGBO(11, 67, 67, 1)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'我的可用积分',
style: TextStyle(color: Colors.white70, fontSize: 14.sp),
),
const SizedBox(width: 4),
GestureDetector(
onTap: (){
controller.toRuleDes();
},
child: const Icon(Icons.help_outline, color: Colors.white70, size: 14),
)
],
),
Text(
'Available points',
style: TextStyle(color: Colors.white38, fontSize: 11.sp),
),
],
),
GestureDetector(
onTap: controller.signAction,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: controller.todaySign.value == 0
? Color.fromRGBO(78, 184, 49, 1)
: Colors.grey,
borderRadius: BorderRadius.circular(10),
),
child: Text(
controller.todaySign.value == 0 ? '立即签到' : '已签到',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
controller.userScore.value.toString(),
style: TextStyle(
color: Color.fromRGBO(236, 236, 236, 1),
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
),
TextButton(
onPressed: () {
controller.toOrders();
},
child: Text(
'历史订单',
style: TextStyle(
color: Color.fromRGBO(148, 163, 184, 1),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
);
}
Widget _buildSectionHeader() {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
Container(
width: 4.w,
height: 16.h,
decoration: BoxDecoration(
color: const Color(0xFF4CAF50),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
'热门商品',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Color.fromRGBO(78, 89, 105, 1),
),
),
],
),
),
);
}
Widget _buildGoodsGrid() {
if (controller.goodsList.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.only(top: 50),
child: Text('暂无商品', style: TextStyle(color: Color(0xFF999999))),
),
),
);
}
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 14.h,
crossAxisSpacing: 19.w,
childAspectRatio: 0.75,
),
delegate: SliverChildBuilderDelegate((context, index) {
final goods = controller.goodsList[index];
return _buildGoodsItem(goods);
}, childCount: controller.goodsList.length),
),
);
}
Widget _buildGoodsItem(GoodsModel goods) {
return Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: goods.goodsImage != null
? Image.network(
goods.goodsImage!,
fit: BoxFit.cover,
width: double.infinity,
)
: Container(
color: const Color(0xFFEEEEEE),
child: const Center(
child: Icon(Icons.image, color: Colors.grey, size: 40),
),
),
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
goods.goodsName,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'${goods.score}',
style: TextStyle(
fontSize: 16.sp,
color: Color(0xFF4CAF50),
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 4),
Text(
'积分',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.bold,
color: Color(0xFF4CAF50),
),
),
],
),
GestureDetector(
onTap: () => controller.exchangeGoods(goods),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFE8F5E9),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'兑换',
style: TextStyle(
color: Color(0xFF4CAF50),
fontWeight: FontWeight.bold,
fontSize: 13.sp,
),
),
),
),
],
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,94 @@
import 'dart:developer';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
class OrderModel {
final int id;
final int scoreGoodsId;
final String goodsName;
final String? goodsImage;
final String? goodsContent;
final String address;
final String createTime;
final String score;
OrderModel({
required this.id,
required this.scoreGoodsId,
required this.goodsName,
this.goodsImage,
this.goodsContent,
required this.address,
required this.createTime,
required this.score,
});
factory OrderModel.fromJson(Map<String, dynamic> json) {
return OrderModel(
id: json['id'] as int,
scoreGoodsId: json['scoreGoodsId'] as int,
goodsName: json['goodsName']?.toString() ?? '',
goodsImage: json['goodsImage'],
goodsContent: json['goodsContent'],
address: json['address']?.toString() ?? '',
createTime: json['createTime']?.toString() ?? '',
score: json['score']?.toString() ?? '',
);
}
}
class MallOrdersController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'mall_orders';
final RxList<OrderModel> orderList = <OrderModel>[].obs;
final RxBool isLoading = true.obs;
int pageNum = 1;
final int pageSize = 50;
@override
void onInit() {
super.onInit();
getOrders();
}
Future<void> getOrders({bool isRefresh = true}) async {
if (isRefresh) {
pageNum = 1;
isLoading.value = true;
updateUi();
}
try {
var response = await HttpService.to.post(
'appointment/score/getScoreExchangeList',
data: {
"status": "",
"pageNum": pageNum.toString(),
"pageSize": pageSize.toString()
},
);
if (response != null && response.data != null) {
var result = BaseModel<Map<String, dynamic>>.fromJson(response.data);
if (result.code == 0 && result.data != null) {
var records = result.data!['records'] as List;
var list = records.map((e) => OrderModel.fromJson(e)).toList();
if (isRefresh) {
orderList.assignAll(list);
} else {
orderList.addAll(list);
}
pageNum++;
}
}
} catch (e) {
log('获取订单列表失败: $e');
} finally {
isLoading.value = false;
updateUi();
}
}
}

View File

@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'controller.dart';
class MallOrdersPage extends GetView<MallOrdersController> {
const MallOrdersPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MallOrdersController>(
init: MallOrdersController(),
id: 'mall_orders',
builder: (_) {
return Scaffold(
backgroundColor: const Color(0xFFF7F8FA),
appBar: AppBar(
title: const Text('历史订单'),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 20),
onPressed: () => Get.back(),
),
),
body: RefreshIndicator(
onRefresh: () => controller.getOrders(isRefresh: true),
child: controller.isLoading.value && controller.orderList.isEmpty
? const Center(child: CircularProgressIndicator())
: _buildOrderList(),
),
);
},
);
}
Widget _buildOrderList() {
if (controller.orderList.isEmpty) {
return const Center(
child: Text('暂无订单记录', style: TextStyle(color: Color(0xFF999999))),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: controller.orderList.length,
itemBuilder: (context, index) {
final order = controller.orderList[index];
return _buildOrderItem(order);
},
);
}
Widget _buildOrderItem(OrderModel order) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'兑换时间:${order.createTime}',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(107, 114, 128, 1),
),
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: order.goodsImage != null
? Image.network(
order.goodsImage!,
width: 80.w,
height: 80.h,
fit: BoxFit.cover,
)
: Container(
width: 80.w,
height: 80.h,
color: Colors.grey[200],
child: const Icon(Icons.image, color: Colors.grey),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
order.goodsName,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
order.score,
style: TextStyle(
fontSize: 16.sp,
color: Color(0xFF4CAF50),
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 4),
Text(
'积分',
style: TextStyle(fontSize: 11.sp, color: Color(0xFF4CAF50)),
),
],
),
],
),
),
Text(
'x1',
style: TextStyle(color: Color(0xFFCCCCCC), fontSize: 16.sp),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/common/index.dart';
import 'package:ln_jq_app/common/login_util.dart';
class MallRulePage extends StatelessWidget {
const MallRulePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color.fromRGBO(64, 199, 154, 1),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white, size: 20),
onPressed: () => Get.back(),
),
),
body: Stack(
children: [
// 顶部装饰图
Positioned(
top: 30,
right: Get.width * 0.15,
child: LoginUtil.getAssImg("rule_bg@2x"),
),
Container(
margin: const EdgeInsets.fromLTRB(20, 100, 20, 20),
padding: const EdgeInsets.all(24),
width: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/rule_bg_1@2x.png'),
fit: BoxFit.fill,
),
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'积分获取规则',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF2C3E50),
),
),
const SizedBox(height: 30),
_buildRuleItem(
icon: 'tips_1@2x',
title: '每日首次签到积分规则',
content: '每日首签,立得 1 积分',
),
_buildRuleItem(
icon: 'tips_2@2x',
title: '每日预约加氢积分规则',
content: '每日前 2 次预约加氢,各得 1 积分',
),
_buildRuleItem(
icon: 'tips_3@2x',
title: '连续签到累计赠分规则',
content: '连续签到 3 天赠 2 积分7 天赠 5 积分',
),
_buildRuleItem(
icon: 'tips_4@2x',
title: '连续签到周期及断签重置规则',
content: '7 天为一个签到周期,中途断签则重新从第 1 天计算',
),
_buildRuleItem(
icon: 'tips_5@2x',
title: '积分使用规则',
content:
'个人账户内累计的所有有效积分,可在平台积分商城中,用于兑换商城内上架的各类商品、权益或服务,兑换时将按照商品标注的积分值扣除对应积分,积分兑换后不支持撤销、退换,商品兑换规则以积分商城内公示为准。',
),
const SizedBox(height: 40),
const Center(
child: Text(
'本活动最终解释权归官方所有,如有疑问可咨询客服。',
style: TextStyle(color: Color(0xFF999999), fontSize: 12),
),
),
],
),
),
),
],
),
);
}
Widget _buildRuleItem({
required String icon,
required String title,
required String content,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LoginUtil.getAssImg(icon),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
],
),
const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.only(left: 0),
child: Text(
content,
style: TextStyle(fontSize: 13.sp, color: Color(0xFF666666), height: 1.5),
),
),
],
),
);
}
}

View File

@@ -12,40 +12,63 @@ class MessagePage extends GetView<MessageController> {
Get.put(MessageController());
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(title: const Text('消息通知'), centerTitle: true),
body: Column(
backgroundColor: const Color(0xFFF7F9FB),
appBar: AppBar(
title: const Text('消息通知'),
centerTitle: true,
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 20),
onPressed: () => Get.back(),
),
),
body: Stack(
children: [
Expanded(
child: Obx(() => SmartRefresher(
controller: controller.refreshController,
enablePullUp: true,
onRefresh: controller.onRefresh,
onLoading: controller.onLoading,
child: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: controller.messageList.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return _buildMessageItem(context, controller.messageList[index]);
},
),
)),
),
Obx(() => SmartRefresher(
controller: controller.refreshController,
enablePullUp: true,
onRefresh: controller.onRefresh,
onLoading: controller.onLoading,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
itemCount: controller.messageList.length,
itemBuilder: (context, index) {
return _buildMessageItem(context, controller.messageList[index]);
},
),
)),
Obx(() => !controller.allRead.value
? Container(
padding: const EdgeInsets.all(16),
color: Colors.white,
child: ElevatedButton(
onPressed: controller.markAllRead,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
minimumSize: const Size(double.infinity, 44),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('全部标为已读', style: TextStyle(fontSize: 16, color: Colors.white)),
),
)
? Positioned(
right: 20,
bottom: 50,
child: GestureDetector(
onTap: controller.markAllRead,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
decoration: BoxDecoration(
color: const Color(0xFF007A45),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Text(
'全部已读',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
),
)
: const SizedBox.shrink()),
],
),
@@ -53,54 +76,92 @@ class MessagePage extends GetView<MessageController> {
}
Widget _buildMessageItem(BuildContext context, MessageModel item) {
return GestureDetector(
onTap: () {
controller.markRead(item);
_showMessageDialog(context, item);
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.only(top: 6, right: 12),
width: 8,
height: 8,
decoration: BoxDecoration(
color: item.isRead == 1 ? Colors.grey[300] : const Color(0xFFFAAD14),
shape: BoxShape.circle,
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 左侧时间轴线条和圆点
SizedBox(
width: 40,
child: Stack(
alignment: Alignment.topCenter,
children: [
Container(
width: 1.5,
color: const Color(0xFFD8E2EE),
),
Positioned(
top: 25,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: item.isRead == 1
? const Color(0xFFAAB6C3)
: const Color(0xFF4CAF50),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
),
],
),
),
// 右侧内容卡片
Expanded(
child: GestureDetector(
onTap: () {
controller.markRead(item);
_showMessageDialog(context, item);
},
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
const SizedBox(height: 10),
Text(
item.content,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
height: 1.4,
),
),
const SizedBox(height: 12),
Text(
item.createTime,
style: const TextStyle(
fontSize: 12,
color: Color(0xFFCCCCCC),
),
),
],
),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87),
),
const SizedBox(height: 8),
Text(
item.content,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
item.createTime,
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
),
],
),
),
],
),
),
],
),
);
}
@@ -111,7 +172,7 @@ class MessagePage extends GetView<MessageController> {
barrierDismissible: true,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
@@ -122,22 +183,22 @@ class MessagePage extends GetView<MessageController> {
item.title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
const SizedBox(height: 16),
Text(
item.content,
style: const TextStyle(fontSize: 15, height: 1.5, color: Colors.black87),
style: const TextStyle(
fontSize: 15, height: 1.5, color: Color(0xFF333333)),
),
const SizedBox(height: 24),
Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
child: TextButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.blue),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF007A45),
),
child: const Text('确认', style: TextStyle(color: Colors.blue)),
child: const Text('确认',
style: TextStyle(fontWeight: FontWeight.bold)),
),
),
],
@@ -147,4 +208,4 @@ class MessagePage extends GetView<MessageController> {
},
);
}
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/common/index.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/c_page/reservation/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';
@@ -36,19 +37,19 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
@override
Widget build(BuildContext context) {
return Container(
height: Get.height * 0.55,
height: Get.height * 0.6,
decoration: const BoxDecoration(
color: Colors.white,
color: Color.fromRGBO(247, 249, 251, 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 构建标题和下拉框
_buildHeader(),
const Divider(height: 1),
// 下拉筛选框
_buildChoice(),
// 构建列表(使用 Obx 监听数据变化)
@@ -58,60 +59,64 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
);
}
Container _buildChoice() {
return Container(
padding: const EdgeInsets.fromLTRB(20, 8, 0, 0),
alignment: AlignmentGeometry.centerLeft,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
Widget _buildChoice() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: _statusOptions.entries.map((entry) {
bool isSelected = _selectedStatus == entry.key;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_selectedStatus == entry.key) return;
// 立即执行刷新逻辑
_controller.getReservationList(addStatus: entry.key);
// 先更新本地状态改变 UI 选中效果
setState(() {
_selectedStatus = entry.key;
});
},
child: Container(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
decoration: BoxDecoration(
// 选中色使用深绿色,未选中保持纯白
color: isSelected ? const Color(0xFF006633) : Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Text(
entry.key == '' ? '全部' : entry.value,
style: TextStyle(
// 未选中文字颜色微调为图片中的灰蓝色
color: isSelected ? Colors.white : const Color(0xFFAAB6C3),
fontSize: 14.sp,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
),
),
),
child: DropdownButton<String>(
value: _selectedStatus,
underline: const SizedBox.shrink(), // 隐藏下划线
items: _statusOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(entry.value),
);
}).toList(),
onChanged: (newValue) {
if (newValue != null) {
setState(() {
_selectedStatus = newValue;
});
// 当选择新状态时,调用接口刷新数据
_controller.getReservationList(addStatus: _selectedStatus);
}
},
),
),
);
);
}).toList(),
),
);
}
/// 构建标题、关闭按钮和下拉筛选框
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.fromLTRB(20, 8, 8, 8),
child: Stack(
alignment: Alignment.center,
children: [
Center(child: const Text('我的预约', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
),
child: const Text('关闭', style: TextStyle(color: Colors.black54)),
),
),
],
margin: const EdgeInsets.fromLTRB(20, 20, 8, 8),
child: const Text(
'我的预约',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
);
}
@@ -134,8 +139,8 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
return Card(
color: Colors.white,
margin: const EdgeInsets.only(bottom: 12.0),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -151,17 +156,20 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
vertical: 5,
),
decoration: BoxDecoration(
color: Colors.blue.shade50, // 淡蓝色背景
color: reservation.state == "-1"
? Color.fromRGBO(241, 67, 56, 0.1)
: Color.fromRGBO(230, 249, 243, 1),
borderRadius: BorderRadius.circular(4), // 小圆角
// 可以选择去掉边框,或者用极淡的边框
border: Border.all(color: Colors.blue.shade100),
),
child: Text(
"${reservation.stateName}-${reservation.addStatusName}",
style: TextStyle(
color: Colors.blue.shade700,
fontSize: 12,
fontWeight: FontWeight.w500,
color: reservation.state == "-1"
? Color.fromRGBO(241, 67, 56, 0.8)
: Color.fromRGBO(49, 186, 133, 1),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
),
),
@@ -171,12 +179,10 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
SizedBox(
height: 28, // 限制按钮高度,显得精致
child: OutlinedButton(
onPressed: () async{
onPressed: () async {
var responseData = await HttpService.to.post(
'appointment/orderAddHyd/vehicle-cancel',
data: {
'id': reservation.id,
},
data: {'id': reservation.id},
);
if (responseData == null || responseData.data == null) {
@@ -198,24 +204,27 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12),
side: BorderSide(color: Colors.grey.shade400), // 灰色边框
shape: const StadiumBorder(), // 胶囊形状
side: BorderSide(color: Colors.grey.shade400),
shape: const StadiumBorder(),
),
child: Text(
'取消预约',
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 11.sp,
),
),
),
),
SizedBox(width: 10.w,),
SizedBox(width: 10.w),
// 修改按钮 (仅在 hasEdit 为 true 时显示)
if (reservation.hasEdit)
SizedBox(
height: 28,
child: OutlinedButton(
onPressed: () async{
onPressed: () async {
var result = await Get.to(
() => ReservationEditPage(),
() => ReservationEditPage(),
arguments: {
'reservation': reservation,
'difference': _controller.difference,
@@ -233,21 +242,22 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12),
side: const BorderSide(color: Colors.blue), // 蓝色边框
side: BorderSide(color: AppTheme.themeColor),
shape: const StadiumBorder(),
backgroundColor: Colors.white,
),
child: const Text(
child: Text(
'修改',
style: TextStyle(color: Colors.blue, fontSize: 12),
style: TextStyle(color: AppTheme.themeColor, fontSize: 11.sp),
),
),),
),
),
],
),
const SizedBox(height: 12),
_buildDetailRow('车牌号:', reservation.plateNumber),
_buildDetailRow('预约日期:', reservation.date),
_buildDetailRow('预约氢量:', reservation.hydAmount),
_buildDetailRow('预约氢量:', "${reservation.hydAmount} KG"),
_buildDetailRow('加氢站:', reservation.stationName),
_buildDetailRow('开始时间:', reservation.startTime),
_buildDetailRow('结束时间:', reservation.endTime),
@@ -271,11 +281,26 @@ class _ReservationListBottomSheetState extends State<ReservationListBottomSheet>
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(color: Colors.grey)),
Text(
label,
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
Text(
value,
style: TextStyle(
color: label.contains("预约氢量:")
? Color.fromRGBO(27, 168, 85, 1)
: Color.fromRGBO(51, 51, 51, 1),
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
],
),