14 Commits

Author SHA1 Message Date
79fe3257b5 状态样式 2026-01-28 17:59:26 +08:00
55569839a7 搜索框 2026-01-28 16:27:37 +08:00
7112d70aba 站点样式 2026-01-28 15:00:30 +08:00
f8a8ecb0ed 司机 预约 样式修改 2026-01-27 17:35:37 +08:00
18c04272e2 车辆信息的部分 2026-01-27 13:57:14 +08:00
14e7fb3d78 首页ui 2026-01-27 13:15:53 +08:00
5ffaf81223 欢迎页 登录 2026-01-27 09:21:36 +08:00
907983a1d1 登录修改 2026-01-26 14:07:58 +08:00
9fdca9136d 导航栏 2026-01-23 17:00:17 +08:00
16bae6a1e9 车辆信息 ui 2026-01-23 15:02:16 +08:00
aabfbfae0c 车辆样式调整 2026-01-23 10:49:02 +08:00
5236670e7c ui修改 2026-01-23 09:22:07 +08:00
cf3ad579d3 新ui调整 2026-01-22 17:29:54 +08:00
70a752b6e5 协议同意 2026-01-21 14:29:03 +08:00
47 changed files with 3763 additions and 2100 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -1,4 +1,5 @@
import 'package:encrypt/encrypt.dart'; import 'package:encrypt/encrypt.dart';
import 'package:flutter/material.dart' as ui;
class LoginUtil { class LoginUtil {
static final _keyString = '915eae87951a448c86c47796e44c1fcf'; static final _keyString = '915eae87951a448c86c47796e44c1fcf';
@@ -26,5 +27,9 @@ class LoginUtil {
final decrypted = _encrypter.decrypt(encrypted); final decrypted = _encrypter.decrypt(encrypted);
return decrypted; return decrypted;
} }
static ui.Image getAssImg(String imgName){
return ui.Image(image: ui.AssetImage('assets/images/$imgName.png'));
}
} }

View File

@@ -5,7 +5,7 @@ class AppTheme {
static const String Font_YuYang = 'YuYang'; static const String Font_YuYang = 'YuYang';
static const Color themeColor = Color(0xFF0c83c3); static const Color themeColor = Color(0xFF017137);
//是否开放域名切换 //是否开放域名切换
static const bool is_show_host = true; static const bool is_show_host = true;

View File

@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
@@ -8,8 +9,8 @@ import 'package:ln_jq_app/storage_service.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'common/styles/theme.dart'; import 'common/styles/theme.dart';
import 'pages/home/view.dart';
import 'pages/login/view.dart'; import 'pages/login/view.dart';
import 'pages/welcome/view.dart'; // 引入启动页
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -17,8 +18,10 @@ void main() async {
WidgetsBinding widgetsBinding = await init( WidgetsBinding widgetsBinding = await init(
isDebug: true, isDebug: true,
logTag: '小羚羚', logTag: '小羚羚',
supportedLocales: [Locale('zh', 'CN')], supportedLocales: [const Locale('zh', 'CN')],
); );
// 保持原生闪屏页,直到 WelcomeController 调用 remove()
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await GetStorage.init(); await GetStorage.init();
@@ -28,36 +31,24 @@ void main() async {
runApp( runApp(
GetxApp( GetxApp(
// 设计稿尺寸 单位dp
designSize: const Size(390, 844), designSize: const Size(390, 844),
// Getx Log
enableLog: true, enableLog: true,
// 默认的跳转动画
defaultTransition: Transition.rightToLeft, defaultTransition: Transition.rightToLeft,
// 主题模式
themeMode: GlobalService.to.themeMode, themeMode: GlobalService.to.themeMode,
// 主题
theme: AppTheme.light, theme: AppTheme.light,
// Dark主题
darkTheme: AppTheme.light, darkTheme: AppTheme.light,
// AppTitle
title: '小羚羚', title: '小羚羚',
// 首页入口 // 将入口改为启动页
home: HomePage(), home: const WelcomePage(),
//组件国际化 fallbackLocale: const Locale('zh', 'CN'),
fallbackLocale: Locale('zh', 'CN'), supportedLocales: const [Locale('zh', 'CN')],
supportedLocales: [Locale('zh', 'CN')],
localizationsDelegates: const [ localizationsDelegates: const [
//pull_to_refresh
RefreshLocalizations.delegate, RefreshLocalizations.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
// Builder
builder: (context, widget) { builder: (context, widget) {
// do something....
return widget!; return widget!;
}, },
), ),
@@ -67,20 +58,16 @@ void main() async {
void initHttpSet() { void initHttpSet() {
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url; AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
// 设置基础 URL
HttpService.to.setBaseUrl(AppTheme.test_service_url); HttpService.to.setBaseUrl(AppTheme.test_service_url);
//指定请求头
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token')); HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
// 设置全局响应处理器
HttpService.to.setOnResponseHandler((response) async { HttpService.to.setOnResponseHandler((response) async {
try { try {
final baseModel = BaseModel.fromJson(response.data); final baseModel = BaseModel.fromJson(response.data);
if (baseModel.code == 0 || baseModel.code == 200) { if (baseModel.code == 0 || baseModel.code == 200) {
return null; return null;
} else if (baseModel.code == 401) { } else if (baseModel.code == 401) {
await StorageService.to.clearLoginInfo(); await StorageService.to.clearLoginInfo();
Get.offAll(() => LoginPage()); Get.offAll(() => const LoginPage());
return baseModel.message; return baseModel.message;
} else { } else {
return (baseModel.error.toString()).isEmpty return (baseModel.error.toString()).isEmpty

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -33,6 +35,9 @@ class ReservationController extends GetxController with BaseControllerMixin {
final TextEditingController broadcastContentController = TextEditingController(); final TextEditingController broadcastContentController = TextEditingController();
final RxInt selectedTabIndex = 0.obs; final RxInt selectedTabIndex = 0.obs;
@override
bool get listenLifecycleEvent => true;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@@ -40,6 +45,39 @@ class ReservationController extends GetxController with BaseControllerMixin {
customStartTime = DateTime.now(); customStartTime = DateTime.now();
customEndTime = customStartTime!.add(const Duration(days: 1)); customEndTime = customStartTime!.add(const Duration(days: 1));
renderData(); renderData();
_msgNotice(); // 红点消息
startAutoRefresh();
}
@override
void onPaused() {
stopAutoRefresh();
super.onPaused();
}
@override
void onClose() {
stopAutoRefresh();
broadcastTitleController.dispose();
broadcastContentController.dispose();
super.onClose();
}
void startAutoRefresh() {
// 先停止已存在的定时器,防止重复启动
stopAutoRefresh();
// 创建一个每5分钟执行一次的周期性定时器
_refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
renderData();
});
}
///停止定时器的方法
void stopAutoRefresh() {
// 如果定时器存在并且是激活状态,就取消它
_refreshTimer?.cancel();
_refreshTimer = null; // 置为null方便判断
} }
String name = ""; String name = "";
@@ -56,6 +94,8 @@ class ReservationController extends GetxController with BaseControllerMixin {
String jobTipStr = ""; String jobTipStr = "";
String jobDetailsStr = ""; String jobDetailsStr = "";
String jobId = ""; String jobId = "";
Timer? _refreshTimer;
bool isNotice = false;
Future<void> renderData() async { Future<void> renderData() async {
showLoading("加载中"); showLoading("加载中");
@@ -65,11 +105,16 @@ class ReservationController extends GetxController with BaseControllerMixin {
if (jobData != null) { if (jobData != null) {
final jobDataResult = BaseModel.fromJson(jobData.data); final jobDataResult = BaseModel.fromJson(jobData.data);
if (jobDataResult.code == 0) { if (jobDataResult.code == 0) {
try{ try {
jobId = jobDataResult.data["id"] ?? ""; final List<dynamic> dataList = jobDataResult.data is List
String endTime = jobDataResult.data["endTime"] ?? ""; ? jobDataResult.data
String beginTime = jobDataResult.data["beginTime"] ?? ""; : [];
String hydStatus = jobDataResult.data["hydStatus"] ?? ""; final firstJob = dataList[0];
jobId = firstJob["id"] ?? "";
String endTime = firstJob["endTime"] ?? "";
String beginTime = firstJob["beginTime"] ?? "";
String hydStatus = firstJob["hydStatus"].toString() ?? "";
String hydStatusStr = ""; String hydStatusStr = "";
if (hydStatus == "0") { if (hydStatus == "0") {
hydStatusStr = "营运中"; hydStatusStr = "营运中";
@@ -81,28 +126,52 @@ class ReservationController extends GetxController with BaseControllerMixin {
hydStatusStr = "暂停营业"; hydStatusStr = "暂停营业";
} }
jobDetailsStr = "当前站点已设置$beginTime-$endTime为$hydStatusStr状态"; //现在的时间晚于开始时间就不显示文案
bool isJobStarted = false;
try {
if (beginTime.isNotEmpty) {
DateTime beginDateTime = DateTime.parse(beginTime);
if (DateTime.now().isAfter(beginDateTime)) {
isJobStarted = true;
}
}
} catch (e) {
print("开始时间解析失败: $e");
}
if (isJobStarted) {
jobTipStr = "";
}
//结束时间
if (endTime.isNotEmpty) { if (endTime.isNotEmpty) {
try { try {
// 解析时间字符串 // 解析时间字符串
DateTime endDateTime = DateTime.parse(endTime); DateTime endDateTime = DateTime.parse(endTime);
DateTime beginDateTime = DateTime.parse(beginTime); DateTime beginDateTime = DateTime.parse(beginTime);
DateTime now = DateTime.now(); // 2. 计算时间差 (endTime - now) DateTime now = DateTime.now(); //计算时间差 (endTime - now)
Duration diff = endDateTime.difference(now); Duration diff = endDateTime.difference(now);
// 计算小时数 (允许小数,例如 0.5) // 计算小时数 (允许小数,例如 0.5)
// inMinutes / 60 可以得到更精确的小数小时 // inMinutes / 60 可以得到更精确的小数小时
double hoursLeft = diff.inMinutes / 60.0; double hoursLeft = diff.inMinutes / 60.0;
if (hoursLeft > 0) { //计算当前时间-开始时间
Duration startDiff = beginDateTime.difference(now);
double hoursUntilStart = startDiff.inMinutes / 60.0;
// 只有在【当前时间早于开始时间】且【剩余时间大于0】时才显示文案
if (now.isBefore(beginDateTime) && hoursLeft > 0) {
// 如果是正数,表示还有多久结束 // 如果是正数,表示还有多久结束
String timeTip = "${hoursLeft.toStringAsFixed(1)}小时后"; String timeTip = " ${hoursUntilStart.toStringAsFixed(2)}小时后";
jobTipStr = "$timeTip$hydStatusStr"; jobTipStr = "$timeTip$hydStatusStr";
} else { } else {
jobTipStr = ""; jobTipStr = "";
} }
jobDetailsStr =
"当前站点已设置$beginTime至$endTime,共${hoursLeft.toStringAsFixed(2)}小时,为$hydStatusStr状态";
// 如果是处于非营运状态,自动回填开始和结束时间 // 如果是处于非营运状态,自动回填开始和结束时间
// 假设 customStartTime 是现在customEndTime 是接口返回的结束时间 // 假设 customStartTime 是现在customEndTime 是接口返回的结束时间
customStartTime = beginDateTime; customStartTime = beginDateTime;
@@ -111,8 +180,10 @@ class ReservationController extends GetxController with BaseControllerMixin {
print("时间解析失败: $e"); print("时间解析失败: $e");
} }
} }
}catch (e){ } catch (e) {
Logger.d("解析失败: $e"); Logger.d("解析失败或者没返回信息: $e");
jobTipStr = "";
} }
} }
} }
@@ -177,6 +248,26 @@ class ReservationController extends GetxController with BaseControllerMixin {
} }
} }
Future<void> _msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
}
}
}
void onOperationStatusChanged(String? newValue) { void onOperationStatusChanged(String? newValue) {
if (newValue != null) { if (newValue != null) {
selectedOperationStatus = newValue; selectedOperationStatus = newValue;
@@ -282,6 +373,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
'appointment/station/updateStationStatus', 'appointment/station/updateStationStatus',
data: { data: {
'hydrogenId': hydrogenId, 'hydrogenId': hydrogenId,
'name': name,
'siteStatus': selectedOperationStatus == "营运中" 'siteStatus': selectedOperationStatus == "营运中"
? "0" ? "0"
: selectedOperationStatus == "维修中" : selectedOperationStatus == "维修中"
@@ -309,6 +401,9 @@ class ReservationController extends GetxController with BaseControllerMixin {
var result = BaseModel.fromJson(responseData.data); var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) { if (result.code == 0) {
showSuccessToast("保存成功,已同步通知对应司机"); showSuccessToast("保存成功,已同步通知对应司机");
//重新刷新页面
renderData();
} }
dismissLoading(); dismissLoading();
} catch (e) { } catch (e) {
@@ -325,10 +420,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
DialogX.to.showConfirmDialog( DialogX.to.showConfirmDialog(
title: '当前设置详情', title: '当前设置详情',
content: Text( content: Text(jobDetailsStr, style: const TextStyle(fontSize: 15, height: 1.5)),
jobDetailsStr,
style: const TextStyle(fontSize: 15, height: 1.5),
),
confirmText: '好的', confirmText: '好的',
cancelText: '取消设置', cancelText: '取消设置',
onCancel: () { onCancel: () {
@@ -342,9 +434,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
void _cancelJob() async { void _cancelJob() async {
showLoading("正在取消..."); showLoading("正在取消...");
try { try {
var response = await HttpService.to.delete( var response = await HttpService.to.delete('appointment/job/hyd/$jobId');
'appointment/job/hyd/$jobId',
);
dismissLoading(); dismissLoading();
if (response != null) { if (response != null) {
@@ -404,11 +494,4 @@ class ReservationController extends GetxController with BaseControllerMixin {
await StorageService.to.clearLoginInfo(); await StorageService.to.clearLoginInfo();
Get.offAll(() => LoginPage()); Get.offAll(() => LoginPage());
} }
@override
void onClose() {
broadcastTitleController.dispose();
broadcastContentController.dispose();
super.onClose();
}
} }

View File

@@ -1,11 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:flutter/services.dart';
import 'package:getx_scaffold/getx_scaffold.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/reservation/controller.dart'; import 'package:ln_jq_app/pages/b_page/reservation/controller.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
class ReservationPage extends GetView<ReservationController> { class ReservationPage extends GetView<ReservationController> {
const ReservationPage({super.key}); const ReservationPage({super.key});
// 定义主题色
static const kPrimaryColor = Color(0xFF006D35); // 效果图深绿色
static const kBgColor = Color(0xFFF5F7F9); // 背景灰
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetBuilder<ReservationController>( return GetBuilder<ReservationController>(
@@ -13,180 +19,309 @@ class ReservationPage extends GetView<ReservationController> {
id: 'b_reservation', id: 'b_reservation',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
backgroundColor: kBgColor,
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildHeaderCard(), _buildTopSection(context),
const SizedBox(height: 12), Padding(
_buildInfoFormCard(context), padding: EdgeInsets.symmetric(horizontal: 20.w),
const SizedBox(height: 12), child: Column(
_buildTipsCard(), children: [
const SizedBox(height: 12), SizedBox(height: 16),
_buildBasicInfoCard(),
SizedBox(height: 16),
_buildOperationContentCard(context),
SizedBox(height: 16.h),
_buildSystemTips(),
SizedBox(height: 24),
_buildLogoutButton(), _buildLogoutButton(),
SizedBox(height: 40),
], ],
), ),
), ),
],
),
), ),
); );
}, },
); );
} }
/// 构建顶部的站点信息头卡片 /// 1. 顶部个人信息及统计栏
Widget _buildHeaderCard() { Widget _buildTopSection(BuildContext context) {
return Card( return Container(
elevation: 2, width: double.infinity,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(30)),
),
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 10,
left: 20,
right: 20,
bottom: 25,
),
child: Column( child: Column(
children: [ children: [
ListTile( Row(
leading: const Icon(Icons.local_gas_station, color: Colors.blue, size: 40),
title: Text(
controller.name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
subtitle: Text(controller.address),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(12),
),
child: Text(
controller.selectedOperationStatus,
style: const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
const Divider(height: 1, indent: 16, endIndent: 16),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
_buildHeaderStat(controller.customerPrice, '氢气价格'), CircleAvatar(
_buildHeaderStat(controller.timeStr, '营业时间'), radius: 25,
_buildHeaderStat('98%', '设备状态'), backgroundColor: Colors.white,
], child: LoginUtil.getAssImg('ic_user_logo@2x'),
), ),
), const SizedBox(width: 12),
], Expanded(
),
);
}
/// 构建头部卡片中的单个统计项
Widget _buildHeaderStat(String value, String label) {
return Column(
children: [
Text(
value,
style: const TextStyle(
color: Colors.blue,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
],
);
}
/// 构建包含所有信息表单的卡片(增加 Tab 切换功能)
Widget _buildInfoFormCard(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
clipBehavior: Clip.antiAlias, // 确保 Tab 背景圆角生效
child: Column(
children: [
// Tab 切换栏
Obx(
() => Container(
color: Colors.grey[50],
child: Row(
children: [
_buildTabItem(0, Icons.business_outlined, '站点信息'),
_buildTabItem(1, Icons.campaign_outlined, '站点广播'),
],
),
),
),
const Divider(height: 1),
// 内容区域
Obx(
() => controller.selectedTabIndex.value == 0
? _buildStationInfo(context)
: _buildStationBroadcast(context),
),
],
),
);
}
/// 构建单个 Tab 项
Widget _buildTabItem(int index, IconData icon, String label) {
bool isSelected = controller.selectedTabIndex.value == index;
return Expanded(
child: InkWell(
onTap: () => controller.selectedTabIndex.value = index,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isSelected ? Colors.blue : Colors.transparent,
width: 2,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 20, color: isSelected ? Colors.blue : Colors.grey[600]),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
fontSize: 15,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.blue : Colors.grey[600],
),
),
],
),
),
),
);
}
/// 站点信息子视图
Widget _buildStationInfo(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildSectionTitle('基本信息'),
_buildDisplayField(label: '站点名称', value: controller.name),
_buildDisplayField(label: '运营企业', value: controller.operatingEnterprise),
_buildDisplayField(label: '站点地址', value: controller.address),
const SizedBox(height: 16),
_buildSectionTitle('价格信息'),
_buildDisplayField(label: '官方价格 (元/kg)', value: controller.customerPrice),
const SizedBox(height: 16),
_buildSectionTitle('运营信息'),
Row( Row(
children: [ children: [
Text('运营状态', style: TextStyle(color: Colors.grey[600], fontSize: 14)), Text(
controller.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
_buildStatusTag(),
],
),
const SizedBox(height: 4),
Text(
"站点:${controller.address}",
style: TextStyle(color: Colors.grey[500], fontSize: 13),
),
],
),
),
IconButton(
onPressed: () {
Get.to(() => const MessagePage());
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
],
),
const SizedBox(height: 25),
Row(
children: [
_buildStatBox("氢气价格", "Hydrogen price", controller.customerPrice, "/kg"),
SizedBox(width: 4.w),
_buildStatBox("营业时间", "Opening time", controller.timeStr, ""),
SizedBox(width: 4.w),
_buildStatBox("设备状态", "Anlagenzustand", "98", "%"),
],
),
],
),
);
}
Widget _buildStatusTag() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFE1F5FE),
borderRadius: BorderRadius.circular(10),
),
child: Text(
controller.selectedOperationStatus,
style: TextStyle(
color: Color(0xFF03A9F4),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
return Expanded(
child: Container(
padding: EdgeInsets.only(left: 12.w, top: 4.h, bottom: 4.h),
decoration: BoxDecoration(
color: kBgColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 12.sp,
color: Color.fromRGBO(51, 51, 51, 0.8),
fontWeight: FontWeight.w400,
),
),
Text(enTitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color(0xFF333333),
),
),
const SizedBox(width: 2),
Text(unit, style: const TextStyle(fontSize: 11, color: Colors.grey)),
],
),
],
),
),
);
}
/// 2. 站点基本信息
Widget _buildBasicInfoCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"站点基本信息",
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 15),
_buildInfoRow("站点名称", controller.name),
_buildInfoRow("运营企业", controller.operatingEnterprise),
_buildInfoRow("站点地址", controller.address),
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(color: Colors.grey, fontSize: 11.sp),
),
Text(
value,
style: TextStyle(
color: Color(0xFF333333),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
/// 3. 运营信息/站点广播 Tab 及内容
Widget _buildOperationContentCard(BuildContext context) {
return GestureDetector(
onTap: hideKeyboard,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
// 自定义 TabBar
Obx(
() => Padding(
padding: const EdgeInsets.only(left: 16, top: 16),
child: Row(
children: [
_buildTabTitle(0, "运营信息"),
const SizedBox(width: 30),
_buildTabTitle(1, "站点广播"),
],
),
),
),
Obx(
() => controller.selectedTabIndex.value == 0
? _buildOperatingForm(context)
: _buildBroadcastForm(),
),
],
),
),
);
}
Widget _buildTabTitle(int index, String title) {
bool isSelected = controller.selectedTabIndex.value == index;
return GestureDetector(
onTap: () => controller.selectedTabIndex.value = index,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.black87 : Colors.grey,
),
),
if (isSelected)
Container(
margin: const EdgeInsets.only(top: 4),
width: 25,
height: 3,
decoration: BoxDecoration(
color: const Color(0xFF00A870), // 效果图中的亮绿色横线
borderRadius: BorderRadius.circular(2),
),
),
],
),
);
}
Widget _buildOperatingForm(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'运营状态',
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
//加氢站未执行的状态修改任务 //加氢站未执行的状态修改任务
if (controller.jobTipStr.isNotEmpty) if (controller.jobTipStr.isNotEmpty)
GestureDetector( GestureDetector(
@@ -203,157 +338,113 @@ class ReservationPage extends GetView<ReservationController> {
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 12),
DropdownButtonFormField<String>( // 状态网格选择
value: controller.selectedOperationStatus, Wrap(
items: controller.operationStatusOptions.map((String value) { spacing: 4,
return DropdownMenuItem<String>(value: value, child: Text(value)); runSpacing: 4,
children: controller.operationStatusOptions.map((status) {
bool isSelected = controller.selectedOperationStatus == status;
return GestureDetector(
onTap: () => controller.onOperationStatusChanged(status),
child: Container(
width: (Get.width - 80) / 2,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? kPrimaryColor : const Color(0xFFEBEBEB),
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
status,
style: TextStyle(
fontSize: 11.sp,
color: isSelected ? Colors.white : Color.fromRGBO(51, 51, 51, 1),
fontWeight: FontWeight.w400,
),
),
),
);
}).toList(), }).toList(),
onChanged: controller.onOperationStatusChanged,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
), ),
), SizedBox(height: 12.h),
const SizedBox(height: 16),
if (controller.selectedOperationStatus == "营运中") if (controller.selectedOperationStatus == "营运中")
_buildDisplayField(label: '营业时间', value: controller.timeStr) _buildDisplayField(label: '营业时间', value: controller.timeStr)
else else
Column( Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildClickField( _buildInputLabel("开始时间"),
label: '开始时间', _buildDateTimePicker(
value: controller.customStartTimeStr, controller.customStartTimeStr,
onTap: () => controller.pickDateTime(context, true), () => controller.pickDateTime(context, true),
), ),
_buildClickField( const SizedBox(height: 15),
label: '结束时间', _buildInputLabel("结束时间"),
value: controller.customEndTimeStr, _buildDateTimePicker(
onTap: () => controller.pickDateTime(context, false), controller.customEndTimeStr,
() => controller.pickDateTime(context, false),
), ),
const SizedBox(height: 15),
], ],
), ),
_buildDisplayField(label: '联系电话', value: controller.phone), _buildDisplayField(label: '联系电话', value: controller.phone),
const SizedBox(height: 24), const SizedBox(height: 25),
ElevatedButton(
onPressed: controller.saveInfo,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('保存信息', style: TextStyle(fontSize: 16)),
),
],
),
);
}
/// 站点广播子视图
Widget _buildStationBroadcast(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row( Row(
children: [ children: [
const Icon(Icons.campaign, color: Colors.blue, size: 28), Expanded(
const SizedBox(width: 10), flex: 1,
const Text( child: OutlinedButton(
'站点广播通知', onPressed: () {
style: TextStyle( controller.renderData();
fontSize: 18, }, // 重置逻辑
fontWeight: FontWeight.bold, style: OutlinedButton.styleFrom(
color: Colors.black87, side: const BorderSide(color: kPrimaryColor),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
), ),
), ),
], child: const Text("重置", style: TextStyle(color: kPrimaryColor)),
),
const SizedBox(height: 13),
_buildTextFieldLabel('通知标题'),
const SizedBox(height: 8),
SizedBox(
height: 45.h,
child: TextField(
controller: controller.broadcastTitleController,
maxLength: 30,
decoration: InputDecoration(
hintText: '例如:临时闭站通知',
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
counterText: '', // 隐藏原生计数器,我们可以按需自定义
), ),
), ),
), const SizedBox(width: 15),
const SizedBox(height: 20), Expanded(
_buildTextFieldLabel('通知内容'), flex: 2,
const SizedBox(height: 8), child: ElevatedButton(
TextField( onPressed: controller.saveInfo,
controller: controller.broadcastContentController,
maxLength: 150,
maxLines: 5,
decoration: InputDecoration(
hintText: '请输入通知内容...',
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: controller.sendBroadcast,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, backgroundColor: kPrimaryColor,
foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12),
minimumSize: const Size(double.infinity, 50), shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), borderRadius: BorderRadius.circular(10),
elevation: 0,
),
child: const Text(
'发送',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
), ),
), ),
const SizedBox(height: 20), child: const Text("保存设置", style: TextStyle(color: Colors.white)),
),
),
],
),
], ],
), ),
); );
} }
Widget _buildTextFieldLabel(String label) {
return Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
);
}
/// 构建带标题的表单区域
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
children: [
Container(width: 4, height: 16, color: Colors.blue),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
);
}
/// 构建一个“标签+纯文本”的显示行
Widget _buildDisplayField({required String label, required String value}) { Widget _buildDisplayField({required String label, required String value}) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12.0), padding: const EdgeInsets.only(bottom: 12.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)), Text(
label,
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Container( Container(
width: double.infinity, width: double.infinity,
@@ -373,118 +464,177 @@ class ReservationPage extends GetView<ReservationController> {
); );
} }
/// 构建一个“可点击”的选择行 Widget _buildBroadcastForm() {
Widget _buildClickField({
required String label,
required String value,
required VoidCallback onTap,
}) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12.0), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)), _buildInputLabel("通知标题"),
const SizedBox(height: 8), TextField(
InkWell( controller: controller.broadcastTitleController,
decoration: _inputDecoration("例如:临时闭站通知"),
),
const SizedBox(height: 15),
_buildInputLabel("通知内容"),
TextField(
controller: controller.broadcastContentController,
maxLines: 4,
decoration: _inputDecoration("请输入通知内容..."),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
flex: 1,
child: OutlinedButton(
onPressed: () {
controller.broadcastTitleController.clear();
controller.broadcastContentController.clear();
}, // 重置逻辑
style: OutlinedButton.styleFrom(
side: const BorderSide(color: kPrimaryColor),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text("重置", style: TextStyle(color: kPrimaryColor)),
),
),
const SizedBox(width: 15),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: controller.sendBroadcast,
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: const Text("发送广播", style: TextStyle(color: Colors.white)),
),
),
],
),
],
),
);
}
Widget _buildInputLabel(String label) {
return Padding(
padding: const EdgeInsets.only(left: 0, bottom: 8),
child: Text(
label,
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _buildDateTimePicker(String value, VoidCallback onTap) {
return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, border: Border.all(color: const Color(0xFF00A870).withOpacity(0.5)),
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.blue.withOpacity(0.5)),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(value, style: const TextStyle(color: Colors.black87)),
const Icon(Icons.calendar_today_outlined, size: 18, color: Color(0xFF00A870)),
],
),
),
);
}
InputDecoration _inputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
);
}
/// 4. 系统提醒
Widget _buildSystemTips() {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: const Color(0xFFF1F9F6), // 极浅绿色背景
borderRadius: BorderRadius.circular(10),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline, color: Color.fromRGBO(1, 113, 55, 1), size: 20),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
value, "系统提醒",
style: const TextStyle(fontSize: 14, color: Colors.black87), style: TextStyle(
color: Color.fromRGBO(1, 113, 55, 1),
fontWeight: FontWeight.bold,
fontSize: 14.sp,
),
),
SizedBox(height: 6.h),
Text(
"请您确保所提供的信息准确无误,价格信息也将实时\n更新至用户端",
style: TextStyle(color: Color.fromRGBO(1, 113, 55, 0.8), fontSize: 12.sp),
),
SizedBox(height: 6.h),
Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(color: Color.fromRGBO(1, 113, 55, 0.8), fontSize: 12.sp),
), ),
const Icon(Icons.calendar_month, size: 18, color: Colors.blue),
], ],
), ),
),
),
], ],
), ),
); );
} }
/// 构建静态提示信息卡片 /// 5. 退出登录按钮
Widget _buildTipsCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_buildInfoItem(Icons.info_outline, '请确保信息准确无误'),
const SizedBox(height: 10),
_buildInfoItem(Icons.help_outline, '价格信息将实时更新到用户端'),
const SizedBox(height: 10),
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系技术支持: 400-021-1773'),
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.verified_outlined, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: FutureBuilder<String>(
future: getVersion(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("");
}
if (snapshot.hasData) {
return TextX.labelSmall(
"当前版本: ${snapshot.data}",
color: Colors.black54,
);
}
return const Text("");
},
),
),
],
),
],
),
),
);
}
/// 构建退出登录按钮
Widget _buildLogoutButton() { Widget _buildLogoutButton() {
return ElevatedButton( return SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: controller.logout, onPressed: controller.logout,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: Color.fromRGBO(204, 52, 46, 1),
foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
minimumSize: const Size(double.infinity, 48), elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 2,
), ),
child: const Text( child: const Text(
'退出登录', "退出登录",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
), ),
);
}
/// 构建带图标的提示信息行
Widget _buildInfoItem(IconData icon, String text) {
return Row(
children: [
Icon(icon, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 14, color: Colors.black54)),
), ),
],
); );
} }
} }

View File

@@ -136,12 +136,13 @@ class SiteController extends GetxController with BaseControllerMixin {
Timer? _refreshTimer; Timer? _refreshTimer;
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
bool isNotice = false;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
renderData(); renderData();
_msgNotice();
startAutoRefresh(); startAutoRefresh();
} }
@@ -152,6 +153,26 @@ class SiteController extends GetxController with BaseControllerMixin {
super.onClose(); super.onClose();
} }
Future<void> _msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
}
}
}
void startAutoRefresh() { void startAutoRefresh() {
// 先停止已存在的定时器,防止重复启动 // 先停止已存在的定时器,防止重复启动
stopAutoRefresh(); stopAutoRefresh();
@@ -580,7 +601,6 @@ class SiteController extends GetxController with BaseControllerMixin {
} }
} catch (e) { } catch (e) {
} finally { } finally {
//加载列表数据 //加载列表数据
fetchReservationData(); fetchReservationData();
} }

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/history/view.dart'; import 'package:ln_jq_app/pages/b_page/history/view.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'controller.dart'; import 'controller.dart';
@@ -15,233 +17,334 @@ class SitePage extends GetView<SiteController> {
init: SiteController(), init: SiteController(),
id: 'site', id: 'site',
builder: (_) { builder: (_) {
return SingleChildScrollView(child: _buildView()); return Scaffold(
backgroundColor: Color.fromRGBO(247, 249, 251, 1),
body: SingleChildScrollView(child: _buildView(context)),
);
}, },
); );
} }
// 主视图 // 主视图
Widget _buildView() { Widget _buildView(BuildContext context) {
return Padding( return Column(
padding: const EdgeInsets.all(12.0), children: [
// 第一个卡片: 今日预约统计
_buildTopSection(context),
Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 第一个卡片: 今日预约统计
Card(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: EdgeInsets.only(top: 17.h, bottom: 10.h),
child: Row( child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
const Icon(Icons.calendar_today, color: Colors.blue, size: 32),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'今日预约统计', "预约信息",
style: TextStyle( style: TextStyle(
fontSize: 16, color: Color.fromRGBO(51, 51, 51, 1),
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
fontSize: 14.sp,
), ),
), ),
Text( GestureDetector(
"Today's Reservation Statistics",
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'实时',
style: TextStyle(
color: Colors.blue,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const SizedBox(height: 10),
const Divider(height: 1, indent: 16, endIndent: 16),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(controller.leftHydrogen, '剩余余量'),
_buildStatItem(controller.orderAmount, '预约车辆'),
_buildStatItem(controller.completedAmount, '已完成'),
],
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(controller.orderTotalAmount, '加氢总量'),
_buildStatItem(controller.orderUnfinishedAmount, '未加氢总量'),
],
),
),
],
),
),
),
// 第二个卡片: 预约信息
Card(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
margin: EdgeInsets.only(bottom: 12),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Container(
color: Colors.blue,
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () { onTap: () {
controller.renderData();
},
child: Row(
children: [
Text(
'今日预约信息',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(
width: 32,
height: 32,
child: const Icon(
Icons.refresh,
size: 18,
color: Colors.white,
),
),
],
),
),
),
ElevatedButton(
onPressed: () {
Get.to( Get.to(
() => HistoryPage(), () => HistoryPage(),
arguments: { arguments: {'stationName': controller.name},
'stationName': controller.name,
},
); );
}, },
style: ElevatedButton.styleFrom( child: Text(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
elevation: 2,
),
child: const Text(
'历史记录', '历史记录',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: Color.fromRGBO(156, 163, 175, 1),
),
), ),
), ),
], ],
), ),
), ),
// 第二个卡片: 预约信息
Column(
children: [
_buildSearchView(), _buildSearchView(),
controller.hasReservationData controller.hasReservationData
? _buildReservationListView() ? _buildReservationListView()
: _buildEmptyReservationView(), : _buildEmptyReservationView(),
], ],
), ),
),
SizedBox(height: 35.h,),
//第三部分 //第三部分
Card( Container(
elevation: 3, padding: const EdgeInsets.all(15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), decoration: BoxDecoration(
margin: const EdgeInsets.only(bottom: 12), color: const Color(0xFFF1F9F6), // 极浅绿色背景
child: Padding( borderRadius: BorderRadius.circular(10),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), ),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline,
color: Color.fromRGBO(1, 113, 55, 1),
size: 20,
),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"系统提醒",
style: TextStyle(
color: Color.fromRGBO(1, 113, 55, 1),
fontWeight: FontWeight.bold,
fontSize: 14.sp,
),
),
SizedBox(height: 6.h),
Text(
"数据每五分钟自动刷新,如需实时更新请下拉页面",
style: TextStyle(
color: Color.fromRGBO(1, 113, 55, 0.8),
fontSize: 12.sp,
),
),
SizedBox(height: 6.h),
Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(
color: Color.fromRGBO(1, 113, 55, 0.8),
fontSize: 12.sp,
),
),
],
),
],
),
),
],
),
),
],
);
}
Widget _buildTopSection(BuildContext context) {
return Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)),
),
padding: EdgeInsets.only(
top: MediaQuery
.of(context)
.padding
.top + 10,
left: 20,
right: 20,
bottom: 25,
),
child: Column(
children: [
Row(
children: [
CircleAvatar(
radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
),
const SizedBox(width: 12),
Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildInfoItem(Icons.info_outline, '数据每5分钟自动刷新一次'), Row(
children: [
Text(
"今日预约统计",
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w400),
),
const SizedBox(width: 8),
],
),
const SizedBox(height: 4),
Text(
"Today's Reservation Statistics",
style: TextStyle(color: Colors.grey[500], fontSize: 13),
),
],
),
),
IconButton(
onPressed: () {
Get.to(() => const MessagePage());
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
],
),
const SizedBox(height: 25),
Row(
children: [
_buildStatBox(
"剩余氢量", "remaining quantity", controller.leftHydrogen, "kg"),
SizedBox(width: 4.w),
_buildStatBox(
"今日加氢量",
"Have been added",
controller.orderTotalAmount,
"kg",
),
SizedBox(width: 4.w),
_buildStatBox(
"未加氢总量",
"No quantity added",
controller.orderUnfinishedAmount,
"kg",
),
],
),
],
),
);
}
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
return Expanded(
child: Container(
padding: EdgeInsets.only(left: 12.w, top: 4.h, bottom: 4.h),
decoration: BoxDecoration(
color: Color(0xFFF5F7F9),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 12.sp,
color: Color.fromRGBO(51, 51, 51, 0.8),
fontWeight: FontWeight.w400,
),
),
Text(enTitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'), Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color(0xFF333333),
),
),
const SizedBox(width: 2),
Text(unit, style: const TextStyle(fontSize: 11, color: Colors.grey)),
], ],
), ),
),
),
], ],
), ),
),
); );
} }
//搜索输入框,提示可以输入车牌或者手机 //搜索输入框,提示可以输入车牌或者手机
Widget _buildSearchView() { Widget _buildSearchView() {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: EdgeInsets.fromLTRB(0, 0, 0, 8.h),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: SizedBox( child: SizedBox(
height: 44, height: 42.h,
child: TextField( child: TextField(
textAlignVertical: TextAlignVertical.center,
controller: controller.searchController, // 绑定控制器 controller: controller.searchController, // 绑定控制器
decoration: InputDecoration( decoration: InputDecoration(
hintText: '输入车牌号或完整手机号查询', filled: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16), isDense: true,
fillColor: Colors.white,
hintText: '输入车牌号或手机号搜索...',
contentPadding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300), borderSide: BorderSide(color: Color.fromRGBO(229, 231, 235, 1)),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300), borderSide: BorderSide(color: Color.fromRGBO(229, 231, 235, 1)),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Get.theme.primaryColor, width: 1.5), borderSide: BorderSide(
color: Color.fromRGBO(229, 231, 235, 1),
width: 1.5,
),
), ),
// 清除按钮 // 清除按钮
suffixIcon: IconButton( suffix: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.clear, size: 20), icon: const Icon(Icons.clear, size: 20),
onPressed: () { onPressed: () {
controller.searchController.clear(); controller.searchController.clear();
controller.fetchReservationData(); // 清除后也刷新一次 controller.fetchReservationData(); // 清除后也刷新一次
}, },
), ),
GestureDetector(
onTap: () {
// 点击“搜索”按钮时触发
FocusScope.of(Get.context!).unfocus(); // 收起键盘
controller.fetchReservationData();
},
child: Container(
padding: EdgeInsets.only(
left: 16.w,
right: 16.h,
top: 4.5.h,
bottom: 4.5.h,
),
decoration: BoxDecoration(
color: Color.fromRGBO(1, 113, 55, 1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
"搜索",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 12.sp,
),
),
),
),
],
),
), ),
onSubmitted: (value) { onSubmitted: (value) {
// 用户在键盘上点击“完成”或“搜索”时触发 // 用户在键盘上点击“完成”或“搜索”时触发
@@ -250,40 +353,6 @@ class SitePage extends GetView<SiteController> {
), ),
), ),
), ),
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}) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: valueColor,
),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
], ],
), ),
); );
@@ -300,7 +369,8 @@ class SitePage extends GetView<SiteController> {
children: [ children: [
Icon(Icons.inventory_2_outlined, size: 80, color: Colors.grey[300]), Icon(Icons.inventory_2_outlined, size: 80, color: Colors.grey[300]),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text('暂无预约数据', style: TextStyle(fontSize: 16, color: Colors.black54)), const Text(
'暂无预约数据', style: TextStyle(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( const Text(
'点击右上角刷新按钮获取最新数据', '点击右上角刷新按钮获取最新数据',
@@ -313,134 +383,289 @@ class SitePage extends GetView<SiteController> {
/// 构建“有预约数据”的列表视图 /// 构建“有预约数据”的列表视图
Widget _buildReservationListView() { Widget _buildReservationListView() {
return Container( return ListView.separated(
color: Colors.white,
child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
// 因为外层已有滚动,这里禁用内部滚动 // 因为外层已有滚动,这里禁用内部滚动
itemCount: controller.reservationList.length, itemCount: controller.reservationList.length,
padding: const EdgeInsets.all(12.0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = controller.reservationList[index]; final item = controller.reservationList[index];
// 调用新的方法来构建每一项 // 调用新的方法来构建每一项
return _buildReservationItem(index, item); return _buildReservationItem(index, item);
}, },
separatorBuilder: (context, index) => const SizedBox(height: 12), // 列表项之间的间距 separatorBuilder: (context, index) => const SizedBox(height: 0), // 列表项之间的间距
),
); );
} }
Widget _buildReservationItem(int index, ReservationModel item) { Widget _buildReservationItem(int index, ReservationModel item) {
const kPrimaryGreen = Color(0xFF006D35); // 深绿
const kLineColor = Color(0xFFE0E6ED); // 线条颜色
return IntrinsicHeight(
// 保证左侧竖线能跟右侧内容高度对齐
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. 左侧样式区域(竖线 + 圆点)
SizedBox(
width: 20,
child: Column(
children: [
// 顶部的装饰圆点
Container(
margin: EdgeInsets.only(top: 3.w),
width: 8,
height: 8,
decoration: BoxDecoration(
color: kPrimaryGreen.withOpacity(0.5),
shape: BoxShape.circle,
),
),
// 延伸的竖线
Expanded(child: Container(width: 1, color: kLineColor)),
],
),
),
// 2. 右侧内容区域(时间 + 卡片数据)
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 10.w, bottom: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 时间显示
Text(
item.time, // 格式如 "09:00 - 10:00"
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
const SizedBox(height: 10),
// 数据卡片
_buildInfoCard(item),
],
),
),
),
],
),
);
}
/// 右侧具体数据卡片
Widget _buildInfoCard(ReservationModel item) {
return Container( return Container(
padding: EdgeInsets.only(left: 16.w, top: 8.5, bottom: 8.5, right: 16.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey[200]!, width: 1.0), border: Border.all(color: const Color(0xFFF0F0F0)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.grey.withOpacity(0.1), color: Colors.black.withOpacity(0.04),
spreadRadius: 1, blurRadius: 10,
blurRadius: 3, offset: const Offset(0, 4),
offset: const Offset(0, 2),
), ),
], ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 顶部:序号、车牌号、状态 // 车牌与状态标签
Padding( Row(
padding: const EdgeInsets.fromLTRB(8, 12, 16, 12),
child: Row(
children: [ children: [
// 序号 Text(
Container( item.plateNumber,
width: 24, style: TextStyle(
height: 24, fontSize: 14.sp,
alignment: Alignment.center, fontWeight: FontWeight.bold,
decoration: const BoxDecoration( color: Color(0xFF333333),
color: Colors.blue, ),
shape: BoxShape.circle, ),
const SizedBox(width: 8),
_buildStatusTag(item.status),
Spacer(),
Text(
"预约量:${item.amount}",
style: TextStyle(
color: Color(0xFF00A870),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
// 联系信息
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"${item.contactPerson} | ${item.contactPhone}",
style: TextStyle(
color: Color(0xFF999999),
fontSize: 12.sp,
fontWeight: FontWeight.w400,
),
),
],
),
//操作按钮(仅在待处理状态显示)
if (item.status == ReservationStatus.pending) ...[
const SizedBox(height: 15),
const Divider(height: 1, color: Color(0xFFF5F5F5)),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildSmallButton(
"拒绝",
isOutline: true,
onTap: () {
controller.rejectReservation(item.id);
},
),
const SizedBox(width: 12),
_buildSmallButton(
"确认",
isOutline: false,
onTap: () {
controller.confirmReservation(item.id);
},
),
],
),
],
],
),
);
}
/// 通用小按钮
Widget _buildSmallButton(String text, {
required bool isOutline,
required VoidCallback onTap,
}) {
const kPrimaryGreen = Color(0xFF006D35);
const kDangerRed = Color(0xFFFF7D7D);
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: isOutline ? Colors.white : kPrimaryGreen,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isOutline ? kDangerRed : kPrimaryGreen),
), ),
child: Text( child: Text(
"${index + 1}", text,
style: const TextStyle( style: TextStyle(
color: Colors.white, color: isOutline ? kDangerRed : Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
);
}
/// 状态标签构建(带圆角和浅色背景)
Widget _buildStatusTag(ReservationStatus status) {
Color textColor;
Color bgColor;
String text;
switch (status) {
case ReservationStatus.pending:
text = "待加氢";
textColor = const Color(0xFFFE9E62); // 橘色
bgColor = const Color(0xFFFFF2E9);
break;
case ReservationStatus.completed: // 假设已加氢状态
text = "已加氢";
textColor = const Color(0xFF00A870); // 绿色
bgColor = const Color(0xFFE6F7F1);
break;
case ReservationStatus.rejected:
text = "拒绝加氢";
textColor = const Color(0xFFFF7D7D); // 红色
bgColor = const Color(0xFFFFEEEE);
break;
case ReservationStatus.unadded:
text = '未加氢';
textColor = const Color(0xFFFF7D7D); // 红色
bgColor = const Color(0xFFFFEEEE);
break;
default:
text = "未知";
textColor = Colors.grey;
bgColor = Colors.grey[100]!;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: textColor.withOpacity(0.3)),
),
child: Text(
text,
style: TextStyle(color: textColor, fontSize: 11, fontWeight: FontWeight.bold),
),
);
}
/// 右侧操作按钮(拒绝/确认)
Widget _buildActionButtons(ReservationModel item) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// 拒绝按钮(空心)
GestureDetector(
onTap: () => controller.rejectReservation(item.id),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFF7D7D)),
),
child: const Text(
"拒绝",
style: TextStyle(
color: Color(0xFFFF7D7D),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 8), const SizedBox(width: 8),
const Icon(Icons.directions_car, color: Colors.black54), // 确认按钮(实心深绿)
const SizedBox(width: 4), GestureDetector(
// 车牌号 onTap: () => controller.confirmReservation(item.id),
Expanded( child: Container(
child: Text( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
item.plateNumber, decoration: BoxDecoration(
style: const TextStyle( color: const Color(0xFF006D35),
fontSize: 16, borderRadius: BorderRadius.circular(10),
),
child: const Text(
"确认",
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppTheme.themeColor,
), ),
), ),
), ),
// 状态标签
_buildStatusChip(item.status),
],
),
),
const Divider(height: 1),
// 中部:详细信息
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Column(
children: [
_buildDetailRow(
Icons.local_gas_station,
'加氢量',
item.amount,
valueColor: Colors.red,
),
const SizedBox(height: 8),
_buildDetailRow(Icons.access_time, '预约时间', item.time),
const SizedBox(height: 8),
_buildDetailRow(Icons.person, '联系人', item.contactPerson),
const SizedBox(height: 8),
_buildDetailRow(
Icons.phone,
'联系电话',
item.contactPhone,
valueColor: AppTheme.themeColor,
),
],
),
),
// 底部:操作按钮 (仅在待处理时显示)
if (item.status == ReservationStatus.pending)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => controller.confirmReservation(item.id),
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
child: const Text('确认'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () => controller.rejectReservation(item.id),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('拒绝'),
),
), ),
], ],
),
),
],
),
); );
} }
@@ -491,8 +716,7 @@ class SitePage extends GetView<SiteController> {
} }
/// 构建信息详情行 /// 构建信息详情行
Widget _buildDetailRow( Widget _buildDetailRow(IconData icon,
IconData icon,
String label, String label,
String value, { String value, {
Color valueColor = Colors.black87, Color valueColor = Colors.black87,

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:getx_scaffold/common/index.dart'; import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.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/car_info/view.dart';
import 'package:ln_jq_app/pages/c_page/map/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/mine/view.dart';
@@ -12,6 +13,7 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
BaseWidgetsPage({super.key}); BaseWidgetsPage({super.key});
final PageController _pageController = PageController(); final PageController _pageController = PageController();
// 主视图 // 主视图
Widget _buildView() { Widget _buildView() {
return PageView( return PageView(
@@ -20,54 +22,68 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
onPageChanged: (index) { onPageChanged: (index) {
jumpTabAndPage(index); jumpTabAndPage(index);
}, },
children: _buildPages(), // 页面的列表 children: _buildPages(),
); );
} }
void jumpTabAndPage(int index) { void jumpTabAndPage(int index) {
controller.pageIndex = index; // 更新页面索引 controller.pageIndex = index;
controller.updateUi(); // 更新 UI controller.updateUi();
_pageController.jumpToPage(controller.pageIndex); _pageController.jumpToPage(controller.pageIndex);
} }
// 对应的页面
List<Widget> _buildPages() { List<Widget> _buildPages() {
return [ return [ReservationPage(), MapPage(), CarInfoPage(), MinePage()];
ReservationPage(),
MapPage(),
CarInfoPage(),
MinePage(),
];
} }
//导航栏 // 自定义导航栏 (悬浮胶囊样式)
Widget _buildNavigationBar() { Widget _buildNavigationBar() {
return NavigationX( return SafeArea(
currentIndex: controller.pageIndex, // 当前选中的tab索引 child: Container(
onTap: (index) { height: 50.h,
jumpTabAndPage(index); margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
}, // 切换tab事件 decoration: BoxDecoration(
items: [ color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
NavigationItemModel( borderRadius: BorderRadius.circular(30),
label: '加氢预约', boxShadow: [
icon: AntdIcon.orderedlist, BoxShadow(
selectedIcon: AntdIcon.calendar_fill, color: Colors.black.withOpacity(0.05),
), blurRadius: 10,
NavigationItemModel( offset: const Offset(0, 5),
label: '地图',
icon: AntdIcon.location,
selectedIcon: AntdIcon.location_fill,
),
NavigationItemModel(
label: '车辆信息',
icon: AntdIcon.car,
selectedIcon: AntdIcon.car_fill,
),
NavigationItemModel(
label: '我的',
icon: AntdIcon.user,
selectedIcon: AntdIcon.user,
), ),
], ],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
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"),
],
),
),
);
}
// 构建单个导航项
Widget _buildNavItem(int index, String icon, String selectedIcon) {
bool isSelected = controller.pageIndex == index;
return GestureDetector(
onTap: () => jumpTabAndPage(index),
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF006633) : Colors.transparent, // 选中时的深绿色背景
borderRadius: BorderRadius.circular(20),
),
child: SizedBox(
height: 24,
width: 24,
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),),
),
); );
} }
@@ -78,10 +94,10 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
id: 'baseWidgets', id: 'baseWidgets',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
extendBody: false, extendBody: true, // 重要:让 body 延伸到导航栏后面
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
bottomNavigationBar: _buildNavigationBar(), bottomNavigationBar: _buildNavigationBar(),
body: SafeArea(child: _buildView()), body: _buildView(), // 移除 SafeArea 以获得更好的全屏沉浸感
); );
}, },
); );

View File

@@ -12,12 +12,12 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Get.put(AttachmentViewerController()); Get.put(AttachmentViewerController());
final fileName = controller.url.split('/').last; // final fileName = controller.url.split('/').last;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
fileName, "证件详情",
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),

View File

@@ -6,7 +6,7 @@ import 'package:path_provider/path_provider.dart';
import 'attachment_viewer_page.dart'; import 'attachment_viewer_page.dart';
class CertificateViewerController extends GetxController with BaseControllerMixin{ class CertificateViewerController extends GetxController with BaseControllerMixin {
late final String title; late final String title;
late final List<String> attachments; late final List<String> attachments;
@@ -78,18 +78,11 @@ class CertificateViewerController extends GetxController with BaseControllerMixi
return; return;
} }
Get.to( Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
() => const AttachmentViewerPage(),
arguments: {
'url': url,
},
);
} }
/// 检查 URL 是否为 PDF (此方法保持不变) /// 检查 URL 是否为 PDF (此方法保持不变)
bool isPdf(String url) { bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf'); return url.toLowerCase().endsWith('.pdf');
} }
} }

View File

@@ -2,10 +2,12 @@ import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart'; import 'package:ln_jq_app/common/model/base_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/c_page/car_info/attachment_viewer_page.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 'package:path_provider/path_provider.dart';
import 'certificate_viewer_page.dart'; import 'certificate_viewer_page.dart';
import 'dart:io';
class CarInfoController extends GetxController with BaseControllerMixin { class CarInfoController extends GetxController with BaseControllerMixin {
@override @override
@@ -22,11 +24,37 @@ class CarInfoController extends GetxController with BaseControllerMixin {
final RxList<String> operationAttachments = <String>[].obs; final RxList<String> operationAttachments = <String>[].obs;
final RxList<String> hydrogenationAttachments = <String>[].obs; final RxList<String> hydrogenationAttachments = <String>[].obs;
final RxList<String> registerAttachments = <String>[].obs; final RxList<String> registerAttachments = <String>[].obs;
String color = "";
String hydrogenCapacity = "";
String rentFromCompany = "";
String address = "";
bool isNotice = false;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
getUserBindCarInfo(); getUserBindCarInfo();
_msgNotice();
}
Future<void> _msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
}
}
} }
@override @override
@@ -95,6 +123,21 @@ class CarInfoController extends GetxController with BaseControllerMixin {
parseAttachments(data['hydrogenationAttachment']), parseAttachments(data['hydrogenationAttachment']),
); );
registerAttachments.assignAll(parseAttachments(data['registerAttachment'])); registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
// 初始化时开始加载所有PDF
attachments = [
...drivingAttachments,
...operationAttachments,
...hydrogenationAttachments,
...registerAttachments,
];
color = data['color'].toString();
hydrogenCapacity = data['hydrogenCapacity'].toString();
rentFromCompany = data['rentFromCompany'].toString();
address = data['address'].toString();
loadAllPdfs();
} }
} }
updateUi(); updateUi();
@@ -116,4 +159,69 @@ class CarInfoController extends GetxController with BaseControllerMixin {
arguments: {'title': title, 'attachments': attachments}, arguments: {'title': title, 'attachments': attachments},
); );
} }
/// 导航到通用的附件查看器页面
void openAttachment(String url) {
if (url.isEmpty) {
showErrorToast('附件链接无效');
return;
}
Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
}
/// 检查 URL 是否为 PDF
bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf');
}
List<String> attachments = [];
// --- 新增: 状态管理 ---
/// 用于存储网络PDF的本地路径key是网络urlvalue是本地路径
final RxMap<String, String> localPdfPaths = <String, String>{}.obs;
/// 用于跟踪每个附件的加载状态key是网络url
final RxMap<String, bool> isLoading = <String, bool>{}.obs;
/// 遍历所有附件如果是PDF则进行下载
void loadAllPdfs() {
for (var url in attachments) {
if (isPdf(url)) {
_downloadPdf(url);
}
}
}
/// 下载单个PDF文件
Future<void> _downloadPdf(String url) async {
if (url.isEmpty) return;
// 开始加载
isLoading[url] = true;
try {
final dio = Dio();
final Directory tempDir = await getTemporaryDirectory();
final String savePath = '${tempDir.path}/${url.split('/').last}';
// 检查文件是否已存在,避免重复下载
if (await File(savePath).exists()) {
localPdfPaths[url] = savePath;
isLoading[url] = false;
return;
}
await dio.download(url, savePath);
// 下载成功后,更新本地路径
localPdfPaths[url] = savePath;
} catch (e) {
print('PDF download error for $url: $e');
// 出错时也可以更新状态以便UI显示错误提示
} finally {
// 结束加载
isLoading[url] = false;
}
}
} }

View File

@@ -1,9 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/qr_code/view.dart'; import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'package:photo_view/photo_view.dart';
import '../../../common/styles/theme.dart';
import 'controller.dart'; import 'controller.dart';
class CarInfoPage extends GetView<CarInfoController> { class CarInfoPage extends GetView<CarInfoController> {
@@ -16,78 +20,256 @@ class CarInfoPage extends GetView<CarInfoController> {
id: 'car_info', id: 'car_info',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
backgroundColor: Colors.grey[100], backgroundColor: const Color.fromRGBO(240, 244, 247, 0.4),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Column(
padding: const EdgeInsets.all(12.0), children: [
_buildUserInfoCard(),
Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildDriverInfoCard(), const SizedBox(height: 16),
const SizedBox(height: 5), _buildCarInfoCard(),
_buildCarBindingCard(), _buildCertificatesCard(context),
const SizedBox(height: 5), const SizedBox(height: 12),
_buildCertificatesCard(), _buildSafetyReminderCard(),
const SizedBox(height: 5), SizedBox(height: 95.h),
_buildTipsCard(),
], ],
), ),
), ),
],
),
), ),
); );
}, },
); );
} }
/// 构建顶部的司机信息卡片 Widget _buildUserInfoCard() {
Widget _buildDriverInfoCard() {
return Card( return Card(
elevation: 2, elevation: 1,
color: Colors.white,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 50),
child: Row( child: Row(
children: [ children: [
const CircleAvatar( Stack(
radius: 20, children: [
backgroundColor: Colors.blue, CircleAvatar(
child: Icon(Icons.person, color: Colors.white, size: 34), radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
), ),
const SizedBox(width: 12), Positioned(
right: 0,
bottom: 0,
child: SizedBox(
height: 16.h,
width: 16.w,
child: LoginUtil.getAssImg('ic_logo@2x'),
),
),
],
),
SizedBox(width: 8.w),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
Text( Text(
"${StorageService.to.name}", "${StorageService.to.name}",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
), ),
const SizedBox(height: 4), ),
SizedBox(width: 8.w),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(236, 255, 234, 1),
border: Border.all(color: const Color(0xFFB7E19F)),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)),
SizedBox(width: 4),
Text( Text(
"${StorageService.to.phone}", "绿色先锋",
style: TextStyle(color: Colors.grey, fontSize: 11), style: TextStyle(
color: Color(0xFF52C41A),
fontSize: 10,
),
), ),
], ],
), ),
), ),
Container( ],
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue, width: 0.5),
), ),
child: const Row( const SizedBox(height: 4),
children: [
Icon(Icons.shield_outlined, color: Colors.blue, size: 14),
SizedBox(width: 4),
Text( Text(
'已认证', "羚牛ID${StorageService.to.phone}",
style: TextStyle( style: const TextStyle(color: Colors.grey, fontSize: 11),
color: Colors.blue, ),
],
),
),
IconButton(
onPressed: () {
Get.to(() => const MessagePage());
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
],
),
),
Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildModernStatItem('本月里程数', 'Accumulated', '2,852km', ''),
const SizedBox(width: 8),
_buildModernStatItem('总里程', 'Refuel Count', "2.5W km", ''),
const SizedBox(width: 8),
_buildModernStatItem('服务评分', 'Driver rating', "4.9分", ''),
],
),
),
],
),
);
}
Widget _buildModernStatItem(String title, String subtitle, String value, String unit) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Text(subtitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.black54)),
],
),
],
),
),
);
}
Widget _buildCarInfoCard() {
return Card(
elevation: 2,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
_buildDetailRow('车牌号', controller.plateNumber, isPlate: true),
const SizedBox(height: 11),
_buildDetailRow('车架号', controller.vin),
const SizedBox(height: 11),
_buildDetailRow('车辆型号', controller.modelName),
const SizedBox(height: 11),
_buildDetailRow('车辆品牌', controller.brandName),
const SizedBox(height: 10),
_buildH2LevelProgress(),
],
),
),
);
}
Widget _buildDetailRow(String label, String value, {bool isPlate = false}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontSize: 13, color: Colors.grey)),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isPlate)
GestureDetector(
onTap: () => controller.doQrCode(),
child: Container(
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
border: Border.all(color: const Color.fromRGBO(71, 174, 208, 1)),
borderRadius: BorderRadius.circular(12),
color: const Color.fromRGBO(235, 250, 255, 1),
),
child: Row(
children: [
const Icon(
Icons.sync,
size: 12,
color: Color.fromRGBO(71, 174, 208, 1),
),
const SizedBox(width: 4),
Text(
StorageService.to.hasVehicleInfo ? "换车牌" : "扫码绑定",
style: const TextStyle(
color: Color.fromRGBO(71, 174, 208, 1),
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -95,227 +277,275 @@ class CarInfoPage extends GetView<CarInfoController> {
], ],
), ),
), ),
],
), ),
), Text(
const Divider(height: 1, indent: 16, endIndent: 16), value,
Padding( style: const TextStyle(
padding: const EdgeInsets.symmetric(vertical: 16.0), fontSize: 14,
child: Row( fontWeight: FontWeight.w600,
mainAxisAlignment: MainAxisAlignment.spaceAround, color: Colors.black87,
children: [
_buildStatItem('156', '服务天数'),
_buildStatItem('4.9', '评分'),
_buildStatItem('98%', '准时率'),
],
), ),
), ),
], ],
), ),
],
); );
} }
// 司机信息卡片中的小统计项 Widget _buildH2LevelProgress() {
Widget _buildStatItem(String value, String label) {
return Column( return Column(
children: [ children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: const LinearProgressIndicator(
value: 0.75,
minHeight: 8,
backgroundColor: Color(0xFFF0F2F5),
valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(16, 185, 129, 1)),
),
),
const SizedBox(height: 8),
const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("H2 Level", style: TextStyle(fontSize: 11, color: Colors.grey)),
Text( Text(
value, "75%",
style: const TextStyle( style: TextStyle(
fontSize: 18, fontSize: 11,
color: Color.fromRGBO(16, 185, 129, 1),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.blue,
), ),
), ),
const SizedBox(height: 4), ],
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)), ),
], ],
); );
} }
/// 构建车辆绑定信息卡片 /// 3. 构建车辆证件卡片 (重构为 TabView 样式)
Widget _buildCarBindingCard() { Widget _buildCertificatesCard(BuildContext context) {
return Card( return DefaultTabController(
elevation: 2, length: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
child: Column( child: Column(
children: [ children: [
_buildInfoRow('车牌号: ${controller.plateNumber}', '扫码绑定'), TabBar(
const SizedBox(height: 12), isScrollable: false,
_buildInfoRow('车架号:', '${controller.vin}'), indicatorColor: Color.fromRGBO(16, 185, 129, 1),
const SizedBox(height: 12), labelColor: Color.fromRGBO(16, 185, 129, 1),
_buildInfoRow('车辆型号:', '${controller.modelName}'), unselectedLabelColor: Colors.grey,
const SizedBox(height: 12), labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
_buildInfoRow('车辆品牌:', '${controller.brandName}'), indicatorSize: TabBarIndicatorSize.label,
tabs: const [
Tab(text: '行驶证'),
Tab(text: '营运证'),
Tab(text: '加氢证'),
Tab(text: '登记证'),
],
),
const SizedBox(height: 9),
SizedBox(
height: 336.h, // 给定一个高度,或者使用别的方式布局
child: TabBarView(
children: [
_buildCertificateContent('行驶证', controller.drivingAttachments),
_buildCertificateContent('营运证', controller.operationAttachments),
_buildCertificateContent('加氢资格证', controller.hydrogenationAttachments),
_buildCertificateContent('登记证', controller.registerAttachments),
], ],
), ),
), ),
const SizedBox(width: 8),
Icon(Icons.propane_rounded, size: 50, color: Colors.blue.withOpacity(0.5)),
], ],
), ),
),
); );
} }
// 车辆绑定卡片中的信息行 /// 构建单个证件的展示内容
Widget _buildInfoRow(String label, String value) { Widget _buildCertificateContent(String title, RxList<String> attachments) {
bool isButton = value == '扫码绑定'; return Obx(() {
return Row( return Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: EdgeInsets.all(16.0),
child: attachments.isEmpty
? const Center(child: Text('暂无相关证件信息'))
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(label, style: const TextStyle(fontSize: 13)), //证件文字
const SizedBox(width: 8), Row(
isButton mainAxisAlignment: MainAxisAlignment.spaceBetween,
? GestureDetector( children: [
onTap: () async { _buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: true),
controller.doQrCode(); _buildCertDetailItem('运营城市', controller.address),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildCertDetailItem(
'车辆颜色',
controller.color,
valueColor: const Color(0xFF52C41A),
),
_buildCertDetailItem('氢瓶容量', controller.hydrogenCapacity),
],
),
const SizedBox(height: 16),
// 附件预览部分
GestureDetector(
onTap: () {
controller.navigateToCertificateViewer(title, attachments);
}, },
child: Container( child: Container(
margin: EdgeInsetsGeometry.only(left: 10.w), height: 184.h,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.blue.shade300, width: 1), borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(5), border: Border.all(color: Color.fromRGBO(226, 232, 240, 1)),
color: Colors.blue.withOpacity(0.05), color: Color.fromRGBO(248, 250, 252, 1),
), ),
child: Row( child: Center(child: _buildAttachmentPreview(attachments[0])),
mainAxisSize: MainAxisSize.min, // Keep the row compact
children: [
Icon(
StorageService.to.hasVehicleInfo ? Icons.repeat : Icons.search,
size: 13,
color: Colors.blue,
), ),
const SizedBox(width: 3),
Text(
StorageService.to.hasVehicleInfo ? "换车牌" : value,
style: const TextStyle(
color: Colors.blue,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
],
),
),
)
: Text(
value,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
),
],
);
}
/// 3. 构建车辆证件卡片
Widget _buildCertificatesCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
children: [
_buildCertificateRow(
icon: Icons.credit_card_rounded,
title: '行驶证',
attachments: controller.drivingAttachments,
),
const Divider(),
_buildCertificateRow(
icon: Icons.article_rounded,
title: '营运证',
attachments: controller.operationAttachments,
),
const Divider(),
_buildCertificateRow(
icon: Icons.propane_tank_rounded,
title: '加氢证',
attachments: controller.hydrogenationAttachments,
),
const Divider(),
_buildCertificateRow(
icon: Icons.app_registration_rounded,
title: '登记证',
attachments: controller.registerAttachments,
), ),
], ],
), ),
), ),
); );
});
} }
/// 证件展示 Widget _buildCertDetailItem(
Widget _buildCertificateRow({ String label,
required IconData icon, String value, {
required String title, Color? valueColor,
required List<String> attachments, bool isFull = false,
}) { }) {
return ListTile( return Container(
contentPadding: EdgeInsets.zero, width: isFull ? null : 140,
leading: CircleAvatar(
radius: 24,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 28),
),
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
// 使用 Obx 响应式地显示附件数量
subtitle: Obx(
() => Text(
'${attachments.length} 个附件',
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
),
trailing: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.find_in_page_outlined, color: AppTheme.themeColor),
),
// 更新 onTap 逻辑
onTap: () => controller.navigateToCertificateViewer(title, attachments),
);
}
Widget _buildTipsCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildTipItem(Icons.info_outline, '请确保车辆证件齐全有效'), Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 10), const SizedBox(height: 4),
_buildTipItem(Icons.rule, '定期检查车辆状态和证件有效期'), Text(
const SizedBox(height: 10), value,
_buildTipItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'), style: TextStyle(
], fontSize: 13,
fontWeight: FontWeight.bold,
color: valueColor ?? Colors.black87,
), ),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
), ),
); );
} }
// 提示信息卡片中的列表项 /// 附件预览组件 (智能判断 PDF 或图片)
Widget _buildTipItem(IconData icon, String text) { Widget _buildAttachmentPreview(String url) {
return Row( return AspectRatio(
aspectRatio: 4 / 3,
child: controller.isPdf(url)
? Obx(() {
final bool loading = controller.isLoading[url] ?? true;
final String? localPath = controller.localPdfPaths[url];
if (loading) {
return _buildLoadingIndicator();
} else if (localPath != null && localPath.isNotEmpty) {
return IgnorePointer(
ignoring: true, // 设置为 true 来忽略所有指针事件
child: PDFView(
backgroundColor: Color.fromRGBO(248, 250, 252, 1),
fitEachPage: true,
filePath: localPath,
fitPolicy: FitPolicy.WIDTH,
// 适配宽度
enableSwipe: false,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
preventLinkNavigation: true, // 顺便禁用PDF内部链接的跳转
),
);
} else {
// PDF加载失败
return _buildErrorIndicator();
}
})
: Image.network(
url,
fit: BoxFit.contain,
// 图片加载时显示loading
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildLoadingIndicator();
},
// 图片加载失败时显示错误
errorBuilder: (context, error, stackTrace) {
return _buildErrorIndicator();
},
),
);
}
Widget _buildLoadingIndicator() {
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
}
Widget _buildErrorIndicator() {
return const SizedBox(
height: 200,
child: Center(child: Icon(Icons.error_outline, color: Colors.red, size: 48)),
);
}
/// 安全提醒卡片
Widget _buildSafetyReminderCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(242, 249, 248, 1),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon(icon, color: Colors.blue, size: 20), Row(
const SizedBox(width: 10), children: [
Expanded( Icon(Icons.info_outline, color: AppTheme.themeColor, size: 24),
child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)), const SizedBox(width: 8),
Text(
"安全提醒",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color.fromRGBO(1, 113, 55, 1),
),
), ),
], ],
),
const SizedBox(height: 12),
const Text(
"请确保车辆证件齐全有效,定期检查车辆状态和证件有效期,以确保运输作业合规安全。",
style: TextStyle(
fontSize: 13,
color: Color.fromRGBO(1, 113, 55, 0.8),
height: 1.5,
),
),
const SizedBox(height: 8),
const Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(fontSize: 13, color: Color.fromRGBO(1, 113, 55, 0.8)),
),
],
),
); );
} }
} }

View File

@@ -35,9 +35,9 @@ class MineController extends GetxController with BaseControllerMixin {
String historyBreakRules = ""; String historyBreakRules = "";
String vin = ""; String vin = "";
String plateNumber = ""; String plateNumber = "";
String violationTotal = "0"; String violationTotal = "0";//违章总数
String violationScore = "0"; String violationScore = "0";//扣分总数
String violationDispose = "0"; String violationDispose = "0";//已处理
bool isNotice = false; bool isNotice = false;
void renderData() async { void renderData() async {

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:getx_scaffold/common/index.dart'; import 'package:getx_scaffold/common/index.dart';
import 'package:getx_scaffold/common/widgets/index.dart'; import 'package:getx_scaffold/common/widgets/index.dart';
import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart'; import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'controller.dart'; import 'controller.dart';
@@ -17,105 +18,160 @@ class MinePage extends GetView<MineController> {
id: 'mine', id: 'mine',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
backgroundColor: Colors.grey[100], backgroundColor: const Color.fromRGBO(247, 249, 251, 1),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildUserInfoCard(), _buildUserInfoCard(),
const SizedBox(height: 5), const SizedBox(height: 8),
_buildDriverScoreCard(), // 新 UI 模块开始
const SizedBox(height: 5), Padding(
_buildMonthlyRecordCard(), padding: const EdgeInsets.symmetric(horizontal: 20.0),
const SizedBox(height: 5), child: Column(
_buildTipsCard(), children: [
const SizedBox(height: 20), _buildWalletCard(),
SizedBox(height: 16.h),
_buildGridMenu(),
SizedBox(height: 16.h),
_buildRecommendCard(context),
SizedBox(height: 8.h),
_buildSafetyReminderCard(),
SizedBox(height: 24.h),
_buildLogoutButton(), _buildLogoutButton(),
const SizedBox(height: 20), SizedBox(height: 95.h),
], ],
), ),
), ),
// 新 UI 模块结束
],
),
), ),
); );
}, },
); );
} }
/// 1. 构建顶部用户信息卡片 /// 构建顶部用户信息卡片
Widget _buildUserInfoCard() { Widget _buildUserInfoCard() {
return Card( return Card(
elevation: 2, elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), color: Colors.white,
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 40),
// 增加了顶部 padding 适配状态栏
child: Row( child: Row(
children: [ children: [
const CircleAvatar( Stack(
children: [
CircleAvatar(
radius: 25, radius: 25,
backgroundColor: Colors.blue, backgroundColor: Colors.white,
child: Icon(Icons.person, color: Colors.white, size: 40), child: LoginUtil.getAssImg('ic_user_logo@2x'),
), ),
const SizedBox(width: 16), Positioned(
right: 0,
bottom: 0,
child: SizedBox(
height: 16.h,
width: 16.w,
child: LoginUtil.getAssImg('ic_logo@2x'),
),
),
],
),
SizedBox(width: 8.w),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
Text( Text(
"${StorageService.to.name}", "${StorageService.to.name}",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8.w),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(236, 255, 234, 1), // 极浅绿色背景
border: Border.all(color: const Color(0xFFB7E19F)), // 边框
borderRadius: BorderRadius.circular(12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)),
// 叶子图标
SizedBox(width: 4),
Text(
"绿色先锋",
style: TextStyle(
color: Color(0xFF52C41A),
fontSize: 10,
),
),
],
),
),
],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
"${StorageService.to.phone}", "羚牛ID${StorageService.to.phone}",
style: TextStyle(color: Colors.grey, fontSize: 11), style: const TextStyle(color: Colors.grey, fontSize: 11),
),
const SizedBox(height: 4),
Text(
StorageService.to.hasVehicleInfo ? "已绑定车辆" : '未绑定车辆',
style: TextStyle(color: Colors.orange, fontSize: 12),
), ),
], ],
), ),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
// 跳转消息中心
Get.to(() => const MessagePage()); Get.to(() => const MessagePage());
}, },
// 这里的 style 是为了模拟你图片里的灰色圆形背景
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.grey[100], backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
), ),
icon: Badge( icon: Badge(
// label: Text('3'), // 如果你想显示数字,就加 label
smallSize: 8, smallSize: 8,
// 红点的大小 backgroundColor: controller.isNotice
backgroundColor: controller.isNotice ? Colors.red : Colors.white, ? Colors.red
// 红点颜色 : Colors.transparent,
child: Icon( child: const Icon(
Icons.notifications_outlined, Icons.notifications_outlined,
color: Colors.black87, color: Colors.black87,
size: 25, size: 30,
), ),
), ),
), ),
], ],
), ),
), ),
const Divider(height: 1),
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_buildStatItem(controller.violationTotal, '违章总数'), _buildModernStatItem('服务天数', 'service days', '156', ''),
_buildStatItem(controller.violationScore, '扣分总数'), const SizedBox(width: 8),
_buildStatItem(controller.violationDispose, '已处理'), _buildModernStatItem('准时率', 'Punctuality', controller.rate, ''),
const SizedBox(width: 8),
_buildModernStatItem('司机评分', 'Driver rating', controller.rating, ''),
], ],
), ),
), ),
@@ -124,202 +180,325 @@ class MinePage extends GetView<MineController> {
); );
} }
// 用户信息卡片中的小统计项 // 统计项
Widget _buildStatItem(String value, String label) { Widget _buildModernStatItem(String title, String subtitle, String value, String unit) {
return Column( return Expanded(
children: [ child: Container(
Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), padding: const EdgeInsets.all(12.0),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
],
);
}
/// 2. 构建驾驶得分卡片
Widget _buildDriverScoreCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'驾驶得分',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const Text('本月表现', style: TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 20),
Center(
child: SizedBox(
width: 100,
height: 100,
child: Stack(
fit: StackFit.expand,
children: [
CircularProgressIndicator(
value: (double.tryParse(controller.rating) ?? 0) / 10,
strokeWidth: 8,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
Center(
child: Text(
controller.rating,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
],
),
),
),
const SizedBox(height: 20),
_buildScoreDetailRow(Icons.directions_car, '安全驾驶', '无违章记录', true),
const Divider(),
_buildScoreDetailRow(Icons.timer, '准时率', '100%准时到达', true),
const Divider(),
_buildScoreDetailRow(Icons.thumb_up, '服务质量', '用户满意度高', true),
const Divider(),
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Row(
children: [
const Text(
'优秀驾驶员',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue, color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(12),
), ),
child: const Text(
'A+',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
],
),
),
],
),
),
);
}
// 驾驶得分卡片中的评分项
Widget _buildScoreDetailRow(
IconData icon,
String title,
String subtitle,
bool isCompleted,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 24),
),
const SizedBox(width: 12),
Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
title, title,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
Text(subtitle, style: const TextStyle(fontSize: 12, color: Colors.grey)), ),
Text(subtitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.black54)),
], ],
), ),
),
if (isCompleted) const Icon(Icons.check_circle, color: Colors.blue),
], ],
), ),
),
); );
} }
/// 3. 构建本月记录卡片 /// 我的钱包卡片
Widget _buildMonthlyRecordCard() { Widget _buildWalletCard() {
return Card( return Card(
elevation: 2, elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'本月记录', "我的钱包",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
Text("User wallet", style: TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
Text(
"¥ 0,00元",
style: TextStyle(
color: Colors.green[700],
fontSize: 16,
fontWeight: FontWeight.bold,
), ),
const SizedBox(height: 8),
_buildRecordRow(Icons.rate_review, '加氢预约践行率', controller.rate),
const Divider(),
_buildRecordRow(
Icons.report_problem_outlined,
'违章',
"${controller.historyBreakRules}",
), ),
const Divider(),
_buildRecordRow(Icons.car_crash_outlined, '交通事故', "${controller.accident}"),
], ],
), ),
), ),
); );
} }
// 本月记录中的列表项 /// 2x2 功能网格菜单
Widget _buildRecordRow(IconData icon, String title, String value) { Widget _buildGridMenu() {
return ListTile( return Column(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
radius: 20,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 24),
),
title: Text(title, style: const TextStyle(fontSize: 14)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(value, style: const TextStyle(color: AppTheme.themeColor, fontSize: 14)),
],
),
onTap: () {
// TODO: 处理点击事件
},
);
}
/// 4. 构建提示信息卡片
Widget _buildTipsCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_buildInfoItem(Icons.info_outline, '保持良好的驾驶习惯,提高安全评分'),
const SizedBox(height: 10),
_buildInfoItem(Icons.rule, '遵守交通规则,避免违章扣分'),
const SizedBox(height: 10),
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
const SizedBox(height: 10),
Row( Row(
children: [ children: [
Icon(Icons.verified_outlined, color: Colors.blue, size: 20), _buildGridItem(Icons.person_search_outlined, "客服评价", "3项可评"),
const SizedBox(width: 10), const SizedBox(width: 19),
_buildGridItem(
Icons.assignment_late_outlined,
"违章处理",
"${controller.historyBreakRules}项待办",
countColor: Colors.red,
),
],
),
const SizedBox(height: 16),
Row(
children: [
_buildGridItem(Icons.book_outlined, "安全培训", "0个待看"),
const SizedBox(width: 19),
_buildGridItem(
Icons.verified_user_outlined,
"诚信加氢值",
"845",
isSpecial: true,
backgroundColor: const Color(0xFF006633),
),
],
),
],
);
}
Widget _buildGridItem(
IconData icon,
String title,
String subtitle, {
Color? countColor,
bool isSpecial = false,
Color? backgroundColor,
}) {
return Expanded(
child: Container(
height: 100,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSpecial ? backgroundColor : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: isSpecial ? Colors.white : Colors.black87, size: 28),
const SizedBox(height: 8),
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: isSpecial ? Colors.white : Colors.black87,
),
),
Text(
subtitle,
style: TextStyle(
fontSize: 12,
color: isSpecial
? Colors.white.withOpacity(0.8)
: (countColor ?? Colors.grey),
),
),
],
),
),
);
}
/// 我要推荐卡片
Widget _buildRecommendCard(BuildContext context) {
return Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"我要推荐",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
Text("Recommend", style: TextStyle(fontSize: 12, color: Colors.grey)),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("累计奖励(积分)", style: TextStyle(fontSize: 11, color: Colors.grey)),
Text(
"0,00",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (GetPlatform.isIOS) {
// 跳转到 iOS 应用商店 (这里使用一个通用的应用商店链接模板,请确保替换为正式的 AppID)
openWebPage("https://apps.apple.com/cn/app/羚牛氢能/6756245815");
} else if (GetPlatform.isAndroid) {
// Android 弹出二维码图片
_showAndroidDownloadDialog(context);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF006633),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 0,
),
child: const Text(
"下载推荐",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
],
),
),
);
}
/// Android 端下载二维码弹窗
void _showAndroidDownloadDialog(BuildContext context) {
Get.dialog(
Center(
child: Container(
width: 280.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const Text(
"扫描二维码下载",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 20),
// 使用 LoginUtil.getAssImg 加载你的图片 android_apk_img.png
SizedBox(
width: 180.w,
height: 180.w,
child: LoginUtil.getAssImg('android_apk_img'),
),
const SizedBox(height: 16),
const Text(
"请让被推荐人扫描上方二维码进行下载安装",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: Colors.grey),
),
],
),
),
const Divider(height: 1),
TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(minimumSize: const Size(double.infinity, 50)),
child: const Text(
"确 定",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
],
),
),
),
);
}
/// 安全提醒卡片
Widget _buildSafetyReminderCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(242, 249, 248, 1), // 极浅绿色背景
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, color: Colors.green[700], size: 24),
const SizedBox(width: 8),
Text(
"安全提醒",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.green[900],
),
),
],
),
const SizedBox(height: 12),
Text(
"请保持良好驾驶习惯,提高安全评分,遵守交通规则,避免违章扣分。",
style: TextStyle(fontSize: 13, color: Colors.green[800], height: 1.5),
),
const SizedBox(height: 8),
Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(fontSize: 13, color: Colors.green[800]),
),
Row(
children: [
Expanded( Expanded(
child: FutureBuilder<String>( child: FutureBuilder<String>(
future: getVersion(), future: getVersion(),
@@ -333,7 +512,7 @@ class MinePage extends GetView<MineController> {
if (snapshot.hasData) { if (snapshot.hasData) {
return TextX.labelSmall( return TextX.labelSmall(
"当前版本: ${snapshot.data}", "当前版本: ${snapshot.data}",
color: Colors.black54, color: Colors.green[800],
); );
} }
@@ -346,20 +525,6 @@ class MinePage extends GetView<MineController> {
), ),
], ],
), ),
),
);
}
// 提示信息卡片中的列表项
Widget _buildInfoItem(IconData icon, String text) {
return Row(
children: [
Icon(icon, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)),
),
],
); );
} }
@@ -369,7 +534,7 @@ class MinePage extends GetView<MineController> {
controller.logout(); controller.logout();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[400], backgroundColor: Color.fromRGBO(204, 52, 46, 1),
foregroundColor: Colors.white, foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48), minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),

View File

@@ -2,17 +2,11 @@ import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/common/common.dart';
import 'package:getx_scaffold/common/services/http.dart';
import 'package:getx_scaffold/getx_scaffold.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/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';
@@ -395,7 +389,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
); );
var result = BaseModel.fromJson(responseData?.data); var result = BaseModel.fromJson(responseData?.data);
if (responseData == null || result.code != 0) { if (responseData == null || result.code != 0) {
dismissLoading(); dismissLoading();
showToast(result.error); showToast(result.error);
@@ -542,6 +535,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
String leftHydrogen = "0"; String leftHydrogen = "0";
num maxHydrogen = 0; num maxHydrogen = 0;
String difference = ""; String difference = "";
var progressValue = 0.0;
//用来管理查看预约的弹窗 //用来管理查看预约的弹窗
Worker? _sheetWorker; Worker? _sheetWorker;
@@ -555,6 +549,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
super.onInit(); super.onInit();
getUserBindCarInfo(); getUserBindCarInfo();
getSiteList(); getSiteList();
_msgNotice();
if (!init) { if (!init) {
_setupListener(); _setupListener();
@@ -562,6 +557,28 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
} }
} }
bool isNotice = false;
Future<void> _msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
}
}
}
@override @override
void dispose() { void dispose() {
_sheetWorker?.dispose(); _sheetWorker?.dispose();
@@ -632,9 +649,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
void getCatinfo() async { void getCatinfo() async {
try { try {
HttpService.to.setBaseUrl(AppTheme.car_service_url);
var responseData = await HttpService.to.post( var responseData = await HttpService.to.post(
'VehicleData/getHydrogenInfoByPlateNumber', 'appointment/vehicle/getHydrogenInfoByPlateNumber',
data: { data: {
'userName': "xll@lingniu", 'userName': "xll@lingniu",
'password': "4q%3!l6s0p", 'password': "4q%3!l6s0p",
@@ -659,11 +675,29 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
amountController.text = flooredDifference.toString(); amountController.text = flooredDifference.toString();
} }
if (maxHydrogen > 0) {
progressValue = leftHydrogenNum / maxHydrogen;
// 边界处理:确保值在 0 到 1 之间
if (progressValue > 1.0) progressValue = 1.0;
if (progressValue < 0.0) progressValue = 0.0;
}
updateUi(); updateUi();
} catch (e) { } catch (e) {
} finally {
HttpService.to.setBaseUrl(AppTheme.test_service_url);
} }
renderSliderTheme();
}
double current = 0.0;
double maxVal = 0.0;
void renderSliderTheme() {
current = double.tryParse(amountController.text) ?? 0.0;
maxVal = double.tryParse(difference) ?? 100.0;
if (maxVal <= 0) maxVal = 100.0;
updateUi();
} }
void getSiteList() async { void getSiteList() async {
@@ -691,7 +725,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
try { try {
showLoading("加氢站数据加载中"); showLoading("加氢站数据加载中");
var responseData = await HttpService.to.get("appointment/station/queryHydrogenSiteInfo"); var responseData = await HttpService.to.get(
"appointment/station/queryHydrogenSiteInfo",
);
if (responseData == null || responseData.data == null) { if (responseData == null || responseData.data == null) {
showToast('暂时无法获取站点信息'); showToast('暂时无法获取站点信息');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get.dart';
class WebController extends GetxController {
late String title;
late String url;
final RxDouble progress = 0.0.obs;
InAppWebViewController? webViewController;
@override
void onInit() {
super.onInit();
// 从参数中获取标题和URL
title = Get.arguments['title'] ?? '详情';
url = Get.arguments['url'] ?? '';
}
void onWebViewCreated(InAppWebViewController controller) {
webViewController = controller;
}
void onProgressChanged(InAppWebViewController controller, int progressValue) {
progress.value = progressValue / 100;
}
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get.dart';
import 'controller.dart';
class WebViewPage extends GetView<WebController> {
const WebViewPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Get.put(WebController());
return Scaffold(
appBar: AppBar(
title: Text(controller.title),
centerTitle: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(2.0),
child: Obx(
() => controller.progress.value < 1.0
? LinearProgressIndicator(
value: controller.progress.value,
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).primaryColor,
),
minHeight: 2.0,
)
: const SizedBox(height: 2.0),
),
),
),
body: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(controller.url)),
initialSettings: InAppWebViewSettings(
isInspectable: true,
javaScriptEnabled: true,
javaScriptCanOpenWindowsAutomatically: true,
useShouldOverrideUrlLoading: true,
mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
),
onWebViewCreated: controller.onWebViewCreated,
onProgressChanged: controller.onProgressChanged,
),
);
}
}

View File

@@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
class LoginController extends GetxController with BaseControllerMixin { class LoginController extends GetxController with BaseControllerMixin {
@override @override
@@ -7,19 +10,73 @@ class LoginController extends GetxController with BaseControllerMixin {
LoginController(); LoginController();
// 控制输入框的 TextEditingController // 控制输入框的 TextEditingController
final TextEditingController driverIdentityController = TextEditingController(); final TextEditingController phoneController = TextEditingController();
final TextEditingController codeController = TextEditingController();
final TextEditingController stationIdController = TextEditingController(); final TextEditingController stationIdController = TextEditingController();
final TextEditingController passwordController = TextEditingController(); final TextEditingController passwordController = TextEditingController();
// --- 倒计时逻辑 ---
final RxInt countdown = 0.obs;
Timer? _timer;
void startCountdown() async {
if (phoneController.text.isEmpty || !phoneController.text.isPhoneNumber) {
showToast("请输入正确的手机号");
return;
}
if (countdown.value > 0) return;
// 调用发送验证码接口
var responseData = await HttpService.to.post(
'appointment/login/sendCode',
data: {"mobile": phoneController.text},
);
if (responseData == null) {
showToast('验证码发送失败,请稍后重试');
return;
}
try {
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
return;
}
showToast("验证码已发送");
countdown.value = 60;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (countdown.value > 0) {
countdown.value--;
} else {
_timer?.cancel();
}
});
} catch (e) {
showToast('验证码服务异常,请稍后重试');
}
}
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
} }
@override @override
void onClose() { void onClose() {
_timer?.cancel();
phoneController.dispose();
codeController.dispose();
stationIdController.dispose();
passwordController.dispose();
super.onClose(); super.onClose();
} }
} }

View File

@@ -1,6 +1,9 @@
import 'dart:io';
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart'; import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart'; import 'package:ln_jq_app/common/login_util.dart';
@@ -9,6 +12,7 @@ import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/common/webview/view.dart';
import 'package:ln_jq_app/pages/login/controller.dart'; import 'package:ln_jq_app/pages/login/controller.dart';
import 'package:ln_jq_app/pages/url_host/view.dart'; import 'package:ln_jq_app/pages/url_host/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
@@ -21,414 +25,142 @@ class LoginPage extends StatefulWidget {
} }
class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin { class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin {
late TabController tabController; late TabController _tabController;
bool _isAgreed = false;
bool cLogin = true;
bool _obscureText = true; bool _obscureText = true;
// 用于管理“记住密码”的复选框状态
bool _rememberPassword = true; bool _rememberPassword = true;
// 用于确保凭证只在首次加载时回填一次
bool _credentialsLoaded = false; bool _credentialsLoaded = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
tabController = TabController(length: 2, vsync: this); _tabController = TabController(length: 2, vsync: this);
tabController.addListener(_tabChangeListener); _tabController.addListener(() {
if (!_tabController.indexIsChanging) {
setState(() {});
} }
void _tabChangeListener() {
if (!tabController.indexIsChanging) {
switchTab(tabController.index);
}
}
void switchTab(int index) {
setState(() {
cLogin = (index == 0);
}); });
} }
@override @override
void dispose() { void dispose() {
tabController.dispose(); _tabController.dispose();
super.dispose(); super.dispose();
} }
Widget _buildView(LoginController controller) {
// 在视图构建时,检查并回填已保存的凭证
if (!_credentialsLoaded) {
final savedAccount = StorageService.to.stationAccount;
final savedPassword = StorageService.to.stationPassword;
if (savedAccount != null && savedPassword != null) {
controller.stationIdController.text = savedAccount;
controller.passwordController.text = savedPassword;
_rememberPassword = true; // 如果有保存的密码,则默认勾选
}
_credentialsLoaded = true; // 标记为已加载,防止重复执行
}
return Container(
color: Color(0xFFEFF4F7),
child: <Widget>[
Icon(cLogin ? AntdIcon.car : AntdIcon.USB),
SizedBox(height: 5.h),
TextX.bodyLarge(cLogin ? '司机端' : "加氢站", weight: FontWeight.w700),
SizedBox(height: 5.h),
TextX.bodyLarge(cLogin ? '安全驾驶·智能服务' : "氢能服务·专业运营"),
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), // 设置圆角弧度
),
margin: EdgeInsets.all(15),
elevation: 4,
child: Container(
height: cLogin ? 285.h : 360.h,
padding: EdgeInsets.all(15),
child: Column(
children: [
Card(
elevation: 2,
child: Container(
height: 55.h,
padding: EdgeInsets.all(3),
child: TabBar(
controller: tabController,
onTap: (index) {
delayed(300, () {
switchTab(index);
});
},
labelColor: Colors.white,
unselectedLabelColor: Colors.black,
indicator: BoxDecoration(
color: AppTheme.themeColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 6,
),
],
),
tabs: [
Tab(text: '司机端登录'),
Tab(text: '加氢站登录'),
],
isScrollable: false,
),
),
),
Flexible(
child: TabBarView(
controller: tabController,
children: [
_driverLoginView(controller),
_stationLoginView(controller),
],
),
),
],
),
),
),
].toColumn(mainAxisSize: MainAxisSize.min).center(),
);
}
Widget _driverLoginView(LoginController controller) {
return !cLogin
? SizedBox()
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 20.h),
TextFormField(
controller: controller.driverIdentityController,
cursorColor: AppTheme.themeColor,
maxLength: 8,
style: TextStyle(fontSize: 14),
decoration: InputDecoration(
hintText: '请输入身份后8位',
border: OutlineInputBorder(),
hintStyle: TextStyle(fontSize: 14),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
prefixIcon: Icon(Icons.person_2_outlined, color: Colors.grey),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppTheme.themeColor),
),
),
),
SizedBox(height: 20.h),
ElevatedButton(
onPressed: () async {
String password = controller.driverIdentityController.text;
if (password.isEmpty) {
showToast("请输入密码");
return;
}
showLoading('登录中...');
try {
var responseData = await HttpService.to.post(
'appointment/login/loginForDriver',
data: {'idNo': password},
);
if (responseData == null && responseData!.data == null) {
dismissLoading();
showToast('登录失败:无法获取凭证');
return;
}
//登录信息处理
try {
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
return;
}
String token = result.data['token'] ?? '';
String idCard = result.data['idCard'] ?? '';
String name = result.data['name'] ?? '';
String phone = result.data['phone'] ?? '';
await StorageService.to.saveLoginInfo(
token: token,
userId: "",
channel: "driver",
idCard: idCard,
name: name,
phone: phone,
);
//注册推送别名
addAlias(phone);
//登录后查询已绑定车辆信息
var carInfo = await HttpService.to.get(
"appointment/driver/getTruckInfoByDriver?phone=$phone",
);
if (carInfo != null) {
var carInforesult = BaseModel.fromJson(carInfo.data);
if (carInforesult.data != null) {
final vehicle = VehicleInfo.fromJson(
carInforesult.data as Map<String, dynamic>,
);
//保存使用
await StorageService.to.saveVehicleInfo(vehicle);
}
}
//页面操作
dismissLoading();
showToast('登录成功,欢迎您');
Get.offAll(() => BaseWidgetsPage());
} catch (e) {
dismissLoading();
showToast('登录失败:数据异常');
}
} catch (e) {
dismissLoading();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.themeColor,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: Text('登录'),
),
],
);
}
Widget _stationLoginView(LoginController controller) {
return cLogin
? SizedBox()
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 20),
TextFormField(
controller: controller.stationIdController,
cursorColor: AppTheme.themeColor,
style: TextStyle(fontSize: 14),
decoration: InputDecoration(
hintText: '请输入加氢站编号',
border: OutlineInputBorder(),
hintStyle: TextStyle(fontSize: 14),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
prefixIcon: Icon(Icons.person_2_outlined, color: Colors.grey),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppTheme.themeColor),
),
),
),
SizedBox(height: 10),
TextFormField(
controller: controller.passwordController,
obscureText: _obscureText,
style: TextStyle(fontSize: 14),
cursorColor: AppTheme.themeColor,
decoration: InputDecoration(
hintStyle: TextStyle(fontSize: 14),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
hintText: '请输入密码',
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(_obscureText ? Icons.visibility_off : Icons.visibility),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
prefixIcon: Icon(Icons.lock_outline, color: Colors.grey),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppTheme.themeColor),
),
),
),
SizedBox(height: 10),
//记住密码复选框 ---
Row(
children: <Widget>[
SizedBox(
height: 24,
width: 24,
child: Checkbox(
value: _rememberPassword,
activeColor: AppTheme.themeColor,
onChanged: (bool? value) {
setState(() {
_rememberPassword = value ?? true;
});
},
),
),
GestureDetector(
onTap: () => setState(() => _rememberPassword = !_rememberPassword),
child: const Text('记住密码', style: TextStyle(color: Colors.grey)),
),
],
),
SizedBox(height: 20), // 调整间距
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.themeColor,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () async {
String account = controller.stationIdController.text;
String password = controller.passwordController.text;
if (account.isEmpty || password.isEmpty) {
showToast("请输入账号和密码");
return;
}
showLoading('登录中...');
try {
String encryptedPassword = LoginUtil.encrypt(password);
var responseData = await HttpService.to.post(
'appointment/login/password',
data: {
'account': account,
'password': encryptedPassword,
'loginType': "station",
},
);
if (responseData == null && responseData!.data == null) {
dismissLoading();
showToast('登录失败:无法获取凭证');
return;
}
try {
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
return;
}
String token = result.data['token'] ?? '';
String userId = result.data['userId'] ?? '';
String mobile = result.data['mobile'] ?? '';
await StorageService.to.saveLoginInfo(
token: token,
userId: userId,
phone: mobile,
channel: "station",
);
//注册推送别名
addAlias(mobile);
// 根据复选框状态保存或清除密码 ---
if (_rememberPassword) {
await StorageService.to.saveStationCredentials(account, password);
} else {
await StorageService.to.clearStationCredentials();
}
dismissLoading();
showToast('登录成功,欢迎您');
Get.offAll(() => B_BaseWidgetsPage());
} catch (e) {
dismissLoading();
showToast('登录失败:数据异常');
}
} catch (e) {
dismissLoading();
}
},
child: Text('登录'),
),
],
);
}
final _aliyunPush = AliyunPushFlutter();
void addAlias(String alias) async {
var result = await _aliyunPush.addAlias(alias);
var code = result['code'];
if (code == kAliyunPushSuccessCode) {
Logger.d('添加别名$alias成功');
} else {
var errorCode = result['code'];
var errorMsg = result['errorMsg'];
Logger.d('添加别名$alias失败: $errorCode - $errorMsg');
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetBuilder<LoginController>( return GetBuilder<LoginController>(
init: LoginController(), init: LoginController(),
id: 'login', id: 'login',
builder: (controller) { builder: (controller) {
// 站点登录凭证回填逻辑
if (!_credentialsLoaded) {
final savedAccount = StorageService.to.stationAccount;
final savedPassword = StorageService.to.stationPassword;
if (savedAccount != null && savedPassword != null) {
controller.stationIdController.text = savedAccount;
controller.passwordController.text = savedPassword;
_rememberPassword = true;
}
_credentialsLoaded = true;
}
return Scaffold( return Scaffold(
body: Stack( backgroundColor: Colors.white,
body: GestureDetector(
onTap: () {
hideKeyboard();
},
child: Stack(
children: [ children: [
Positioned.fill(child: _buildView(controller)), // 1. 顶部背景与装饰
Positioned(
top: 0,
left: 0,
right: 0,
child: LoginUtil.getAssImg("bg_login"),
),
Positioned(
top: 0,
left: 0,
child: SizedBox(
width: 180.w,
height: 218.h,
child: LoginUtil.getAssImg("ic_login_bg@2x"),
),
),
_buildBrandingHeader(),
// 2. 登录表单主体
Positioned(
top: 280.h,
left: 0,
right: 0,
bottom: 0,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(40),
topRight: Radius.circular(40),
),
),
child: Column(
children: [
const SizedBox(height: 20),
// TabBar 切换
Container(
margin: EdgeInsets.symmetric(horizontal: 60.w),
child: TabBar(
controller: _tabController,
indicatorColor: const Color(0xFF006633),
indicatorWeight: 3,
labelColor: const Color(0xFF006633),
unselectedLabelColor: Colors.grey,
labelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
tabs: const [
Tab(text: "司机登录"),
Tab(text: "站点登录"),
],
),
),
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 30.w),
child: Column(
children: [
const SizedBox(height: 30),
// 根据 Tab 显示不同的输入框
_tabController.index == 0
? _buildDriverInputFields(controller)
: _buildStationInputFields(controller),
const SizedBox(height: 30),
// 统一登录按钮
_buildLoginButton(controller),
const SizedBox(height: 10),
buildAgreement(),
const SizedBox(height: 40),
],
),
),
),
],
),
),
),
Positioned(left: 0, right: 0, bottom: 33.h, child: _buildFooterSlogan()),
if (AppTheme.is_show_host) if (AppTheme.is_show_host)
Positioned( Positioned(
top: 40.h, top: 40.h,
right: 20.w, right: 20.w,
child: TextButton( child: TextButton(
onPressed: () { onPressed: () => Get.to(() => const UrlHostPage()),
Get.to(() => const UrlHostPage());
},
child: const Text( child: const Text(
"域名配置", "域名配置",
style: TextStyle( style: TextStyle(
@@ -441,8 +173,461 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
), ),
], ],
), ),
),
); );
}, },
); );
} }
/// 品牌头部
Widget _buildBrandingHeader() {
return Positioned(
top: 0,
left: 32.w,
right: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 100.h),
SizedBox(height: 60.h, child: LoginUtil.getAssImg('ic_logo_unbg@2x')),
SizedBox(height: 30.h),
const Text(
"HELLO,",
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(51, 51, 51, 1),
),
),
const SizedBox(height: 8),
Row(
children: [
const Text(
"欢迎使用 ",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(51, 51, 51, 1),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: const Color.fromRGBO(56, 198, 151, 1),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
"“羚牛氢能智慧服务平台”",
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
);
}
/// 司机登录输入框 (手机号+验证码)
Widget _buildDriverInputFields(LoginController controller) {
return Column(
children: [
_buildInputWrapper(
child: TextField(
controller: controller.phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
hintText: '请输入手机号',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 24),
),
),
),
const SizedBox(height: 20),
_buildInputWrapper(
child: Row(
children: [
Expanded(
child: TextField(
controller: controller.codeController,
keyboardType: TextInputType.number,
inputFormatters: [LengthLimitingTextInputFormatter(6)],
decoration: const InputDecoration(
hintText: '请输入验证码',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 24),
),
),
),
Obx(
() => GestureDetector(
onTap: controller.countdown.value == 0
? controller.startCountdown
: null,
child: Padding(
padding: const EdgeInsets.only(right: 24.0),
child: Text(
controller.countdown.value == 0
? "获取验证码"
: "${controller.countdown.value}s后重发",
style: TextStyle(
color: controller.countdown.value == 0
? const Color(0xFF006633)
: Colors.grey,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
],
);
}
/// 站点登录输入框 (账号+密码)
Widget _buildStationInputFields(LoginController controller) {
return Column(
children: [
_buildInputWrapper(
child: TextField(
controller: controller.stationIdController,
decoration: const InputDecoration(
hintText: '请输入加氢站编号',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 24),
),
),
),
const SizedBox(height: 20),
_buildInputWrapper(
child: Row(
children: [
Expanded(
child: TextField(
controller: controller.passwordController,
obscureText: _obscureText,
decoration: const InputDecoration(
hintText: '请输入密码',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 24),
),
),
),
IconButton(
icon: Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: () => setState(() => _obscureText = !_obscureText),
),
],
),
),
const SizedBox(height: 10),
Row(
children: [
SizedBox(
width: 40,
child: Checkbox(
value: _rememberPassword,
activeColor: const Color(0xFF006633),
onChanged: (val) => setState(() => _rememberPassword = val ?? false),
),
),
const Text("记住密码", style: TextStyle(color: Colors.grey, fontSize: 14)),
],
),
],
);
}
/// 通用输入框包装
Widget _buildInputWrapper({required Widget child}) {
return Container(
height: 55.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFFF7F9FB),
borderRadius: BorderRadius.circular(28),
),
child: child,
);
}
/// 统一登录按钮逻辑
Widget _buildLoginButton(LoginController controller) {
return ElevatedButton(
onPressed: () => _handleLogin(controller),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF006633),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 55),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
elevation: 0,
),
child: Text(
_tabController.index == 0 ? "司机登录" : "站点登录",
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
);
}
void _handleLogin(LoginController controller) async {
if (!_isAgreed) {
DialogX.to.showConfirmDialog(
icon: DialogIcon.warn,
content: _buildDialogContent(),
confirmText: '同意',
cancelText: '拒绝',
onConfirm: () {
_isAgreed = true;
controller.updateUi();
},
);
return;
}
_tabController.index == 0
? _handleDriverLogin(controller)
: _handleStationLogin(controller);
}
/// 司机登录逻辑 (短信登录)
void _handleDriverLogin(LoginController controller) async {
if (!_validateAgreed()) return;
String phone = controller.phoneController.text;
String code = controller.codeController.text;
if (phone.isEmpty || !phone.isPhoneNumber) {
showToast("请输入正确的手机号");
return;
}
if (code.isEmpty) {
showToast("请输入验证码");
return;
}
showLoading('登录中...');
try {
var responseData = await HttpService.to.post(
'appointment/login/login',
data: {'mobile': phone, 'code': code},
);
_processLoginResponse(responseData, "driver", phone);
} catch (e) {
dismissLoading();
}
}
/// 站点登录逻辑 (账号密码)
void _handleStationLogin(LoginController controller) async {
if (!_validateAgreed()) return;
String account = controller.stationIdController.text;
String password = controller.passwordController.text;
if (account.isEmpty || password.isEmpty) {
showToast("请输入账号和密码");
return;
}
showLoading('登录中...');
try {
String encryptedPassword = LoginUtil.encrypt(password);
var responseData = await HttpService.to.post(
'appointment/login/password',
data: {'account': account, 'password': encryptedPassword, 'loginType': "station"},
);
if (_rememberPassword) {
await StorageService.to.saveStationCredentials(account, password);
} else {
await StorageService.to.clearStationCredentials();
}
_processLoginResponse(responseData, "station", account);
} catch (e) {
dismissLoading();
}
}
bool _validateAgreed() {
if (!_isAgreed) {
DialogX.to.showConfirmDialog(
icon: DialogIcon.warn,
message: '请阅读并同意用户协议和隐私政策',
confirmText: '确定',
onConfirm: () {},
);
return false;
}
return true;
}
void _processLoginResponse(
dynamic responseData,
String channel,
String identifier,
) async {
if (responseData == null || responseData.data == null) {
dismissLoading();
showToast('登录失败');
return;
}
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
return;
}
String token = result.data['token'] ?? '';
if (channel == "driver") {
await StorageService.to.saveLoginInfo(
token: token,
userId: "",
channel: "driver",
name: result.data['name'],
phone: result.data['phone'],
);
// 成功后自动获取车辆信息
var carInfo = await HttpService.to.get(
"appointment/driver/getTruckInfoByDriver?phone=${result.data['phone']}",
);
if (carInfo != null) {
var carResult = BaseModel.fromJson(carInfo.data);
if (carResult.data != null)
await StorageService.to.saveVehicleInfo(VehicleInfo.fromJson(carResult.data));
}
dismissLoading();
Get.offAll(() => BaseWidgetsPage());
} else {
await StorageService.to.saveLoginInfo(
token: token,
userId: result.data['userId'],
phone: result.data['mobile'],
channel: "station",
);
dismissLoading();
Get.offAll(() => B_BaseWidgetsPage());
}
addAlias(identifier);
}
final _aliyunPush = AliyunPushFlutter();
void addAlias(String alias) async {
await _aliyunPush.addAlias(alias);
}
Widget buildAgreement() {
return Padding(
padding: EdgeInsets.only(top: 13.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 勾选框
SizedBox(
width: 22.w,
height: 22.h,
child: Checkbox(
value: _isAgreed,
activeColor: AppTheme.themeColor,
// 简单的圆角样式
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
onChanged: (bool? value) {
setState(() {
_isAgreed = value ?? false;
});
},
),
),
const SizedBox(width: 4),
// 富文本协议部分
Text.rich(
TextSpan(
text: '我已阅读并同意',
style: const TextStyle(color: Colors.grey, fontSize: 13),
children: [
TextSpan(
text: '《用户协议》',
style: TextStyle(color: AppTheme.themeColor, fontSize: 13),
recognizer: TapGestureRecognizer()
..onTap = () {
openPage("用户协议", "https://lnh2e.com/user_agreement.html");
},
),
const TextSpan(text: ''),
TextSpan(
text: '《隐私政策》',
style: TextStyle(color: AppTheme.themeColor, fontSize: 13),
recognizer: TapGestureRecognizer()
..onTap = () {
openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html");
},
),
],
),
),
],
),
);
}
Widget _buildDialogContent() {
return RichTextX(
children: [
TextSpanItem('请阅读并同意'),
TextSpanItem(
'《隐私协议》',
onTap: () => openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html"),
),
TextSpanItem(''),
TextSpanItem(
'《用户政策》',
onTap: () => openPage("用户协议", "https://lnh2e.com/user_agreement.html"),
),
TextSpanItem(',我们将在协议框架内为您提供更优质的服务。'),
],
);
}
void openPage(String title, String url) {
if (Platform.isIOS) {
openWebPage(url);
return;
}
Get.to(() => const WebViewPage(), arguments: {'title': title, 'url': url});
}
Widget _buildFooterSlogan() {
return Center(
child: Column(
children: [
Text(
"H Y P A I",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: const Color.fromRGBO(51, 51, 51, 1),
letterSpacing: 8,
),
),
Text(
"HYDROGEN MOBILITY",
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 9,
color: Colors.grey.shade400,
letterSpacing: 1,
),
),
],
),
);
}
} }

View File

@@ -10,12 +10,14 @@ class UrlHostController extends GetxController {
// 预设的域名列表 // 预设的域名列表
final List<String> presetUrls = [ final List<String> presetUrls = [
'https://beta-esg.api.lnh2e.com/', // 测试环境 'https://beta-esg.api.lnh2e.com/', // 测试环境
'http://47.101.201.13:8443/api/', // 测试环境
'http://192.168.110.44:8080/', // 沈辰本地 'http://192.168.110.44:8080/', // 沈辰本地
'http://192.168.110.222:8080/', // 何斐本地 'http://192.168.110.222:8080/', // 何斐本地
]; ];
final List<String> urlNames = [ final List<String> urlNames = [
'测试环境', '测试环境',
'线上环境',
'沈辰本地环境', '沈辰本地环境',
'何斐本地环境', '何斐本地环境',
]; ];

View File

@@ -0,0 +1,32 @@
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/pages/home/view.dart';
import 'package:ln_jq_app/pages/login/view.dart';
import 'package:ln_jq_app/storage_service.dart';
class WelcomeController extends GetxController {
@override
void onReady() {
super.onReady();
// 移除原生闪屏页(如果有的话)
FlutterNativeSplash.remove();
_startTimer();
}
void _startTimer() {
// 1.5秒后执行跳转逻辑
Future.delayed(const Duration(milliseconds: 1500), () {
Get.offAll(() => const HomePage());
});
}
void _jumpToNextPage() {
if (StorageService.to.isLoggedIn) {
// 已登录,跳转到首页
Get.offAll(() => const HomePage());
} else {
// 未登录,跳转到登录页
Get.offAll(() => const LoginPage());
}
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'controller.dart';
class WelcomePage extends GetView<WelcomeController> {
const WelcomePage({super.key});
@override
Widget build(BuildContext context) {
// 初始化控制器
Get.put(WelcomeController());
return Scaffold(
backgroundColor: Colors.white,
body: SizedBox.expand(
child: Stack(
children: [
Positioned(
top: 0,
bottom: 0,
left: 0,
right: 0,
child: Image.asset(
'assets/images/welcome.png',
fit: BoxFit.fill
),
),
],
),
),
);
}
}