QinGang_interested/lib/pages/home/home_page.dart

844 lines
29 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// home_page.dart (适配新菜单配置)
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/common/route_aware_state.dart';
import 'package:qhd_prevention/common/route_service.dart';
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/pages/home/Study/study_tab_list_page.dart';
import 'package:qhd_prevention/pages/home/scan_page.dart';
import 'package:qhd_prevention/pages/home/unit/unit_tab_page.dart';
import 'package:qhd_prevention/pages/main_tab.dart';
import 'package:qhd_prevention/pages/mine/onboarding_full_page.dart';
import 'package:qhd_prevention/pages/user/choose_userFirm_page.dart';
import 'package:qhd_prevention/pages/user/firm_list_page.dart';
import 'package:qhd_prevention/services/auth_service.dart';
import 'package:qhd_prevention/services/scan_service.dart';
import 'package:qhd_prevention/tools/h_colors.dart';
import 'package:qhd_prevention/tools/tools.dart';
import 'package:shared_preferences/shared_preferences.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key, required this.isChooseFirm}) : super(key: key);
final bool isChooseFirm;
@override
HomePageState createState() => HomePageState();
}
class HomePageState extends RouteAwareState<HomePage>
with WidgetsBindingObserver, SingleTickerProviderStateMixin {
final PageController _pageController = PageController();
final ScrollController _scrollController = ScrollController();
bool _isShowCheckLogin = false;
int _currentPage = 0;
bool _isMobileSelected = true;
void startScan() async {
final result = await pushPage(
ScanPage(type: ScanType.Onboarding),
context,
);
if (result == null) return;
ScanService.scan(context, result);
}
static const String _hiddenCacheKey = 'hidden_roll_cache';
// 上面按钮显示状态(与 buttonInfos 顺序对应)
List<bool> _buttonVisibility = [];
// 页面模块可见性(由路由驱动)
bool _showNotificationBar = true;
bool _showWorkStats = true;
bool _showCheckList = true;
List totalList = [];
Map<String, int> workStats = {'total': 36, 'processed': 30, 'pending': 6};
List<Map<String, dynamic>> checkLists = [
{
"title": "电工班车间清单",
"type": "隐患排查",
"time": "2025-11-17",
"color": Color(0xFF4CAF50),
},
{
"title": "工地区消防点检清单",
"type": "消防点检",
"time": "2025-11-17",
"color": Color(0xFF2196F3),
},
{
"title": "消防专项检查清单",
"type": "重点作业",
"time": "2025-11-17",
"color": Color(0xFFFF9800),
},
];
final List<String> _notifications = [
"系统通知今晚20:00 将进行系统维护,请提前保存数据。",
"安全提示:施工区请佩戴安全帽并系好安全带。",
"公告本周五集团例会在多功能厅召开9:00准时开始。",
"提醒:请尽快完成隐患整改清单中的待办项。",
];
int _notifIndex = 0;
late final PageController _notifPageController;
Timer? _notifTimer;
// 图标按钮定义(顺序固定)
List<Map<String, dynamic>> buttonInfos = [
{"icon": "assets/images/ico1.png", "title": "单位管理", "unreadCount": 0},
{"icon": "assets/images/ico2.png", "title": "现场监管", "unreadCount": 0},
{"icon": "assets/images/ico3.png", "title": "危险作业", "unreadCount": 0},
{"icon": "assets/images/ico4.png", "title": "隐患治理", "unreadCount": 0}, // 原“隐患处理”改为“隐患治理”
{"icon": "assets/images/ico5.png", "title": "重点作业", "unreadCount": 0},
{"icon": "assets/images/ico6.png", "title": "口门门禁", "unreadCount": 0},
{"icon": "assets/images/ico7.png", "title": "入港培训", "unreadCount": 0},
];
// 浮动 AppBar
bool _showFloatingAppBar = false;
static const double _triggerOffset = 30.0;
static const double _floatingBarHeight = 56.0;
// 按钮标题 -> 权限标识映射(基于新菜单配置)
final Map<String, String> _titleToPerm = {
"单位管理": "dashboard-Unit-Management",
"现场监管": "dashboard-Site-Supervision",
"危险作业": "dashboard-Hazardous-Work",
"隐患治理": "dashboard-Hazard-Management",
"重点作业": "", // 无对应,暂时留空
"口门门禁": "dashboard-Gate-Access-Control",
"入港培训": "dashboard-Study-Training",
};
// 模块权限映射(基于新菜单配置)
final Map<String, String> _modulePerms = {
"notification": "dashboard-roll-notice",
"todoStats": "dashboard-todo-sort",
"checklist": "dashboard-todo-list",
"scan": "dashboard-scan",
"joinFirm": "dashboard-start-work",
};
@override
void initState() {
super.initState();
_isShowCheckLogin = widget.isChooseFirm;
// 初始按钮全隐藏,避免闪烁
_buttonVisibility = List<bool>.filled(buttonInfos.length, false);
_notifPageController = PageController(initialPage: 0);
_notifTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
if (!mounted) return;
final next = (_notifIndex + 1) % _notifications.length;
_notifIndex = next;
try {
_notifPageController.animateToPage(
next,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
} catch (_) {}
_getNeedSafetyCommitment();
setState(() {});
});
_scrollController.addListener(_onScroll);
WidgetsBinding.instance.addObserver(this);
RouteService().addListener(onRouteConfigLoaded);
Future.microtask(() {
_updateModuleAndButtonVisibility();
});
}
@override
void dispose() {
_notifTimer?.cancel();
_notifPageController.dispose();
_pageController.dispose();
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
WidgetsBinding.instance.removeObserver(this);
try {
RouteService().removeListener(onRouteConfigLoaded);
} catch (_) {}
super.dispose();
}
void onRouteConfigLoaded() {
if (!mounted) return;
_updateModuleAndButtonVisibility();
}
/// 根据路由配置更新按钮和模块可见性
void _updateModuleAndButtonVisibility() {
final routeService = RouteService();
final mainTabs = routeService.mainTabs;
if (mainTabs.isEmpty) {
// 路由未加载,保持全隐藏
return;
}
// 更新按钮可见性
final List<bool> newVisibility = List<bool>.filled(buttonInfos.length, false);
for (int i = 0; i < buttonInfos.length; i++) {
final title = buttonInfos[i]['title'] as String;
final perm = _titleToPerm[title];
if (perm == null || perm.isEmpty) continue;
try {
newVisibility[i] = routeService.hasPerm(perm);
} catch (e) {
debugPrint('权限检查异常: $perm, $e');
}
}
// 更新模块可见性
final bool showNotif = routeService.hasPerm(_modulePerms['notification']!);
final bool showTodo = routeService.hasPerm(_modulePerms['todoStats']!);
final bool showChecklist = routeService.hasPerm(_modulePerms['checklist']!);
setState(() {
_buttonVisibility = newVisibility;
_showNotificationBar = showNotif;
_showWorkStats = showTodo;
_showCheckList = showChecklist;
});
}
Future<void> _getNeedSafetyCommitment() async {
if (_isShowCheckLogin) return;
final prefs = await SharedPreferences.getInstance();
final phone = prefs.getString('savePhone') ?? '';
final pwd = prefs.getString('savePass') ?? '';
final result = await AuthApi.userLoginCheckFirm({'phone': phone});
if (result['success']) {
final resData = result['data'];
List firmList = resData['userCorpInfoCOList'] ?? [];
if (firmList.isNotEmpty) {
_isShowCheckLogin = true;
CustomAlertDialog.showAlert(
context,
title: '温馨提示',
content: '您的入职申请已通过',
confirmText: '立即入职',
onConfirm: () async {
Map data = {'unitId': firmList.first['corpinfoId'] ?? ''};
final res = await AuthService.gbsLogin(phone, pwd, data);
LoadingDialogHelper.hide();
if (res['success'] == true) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const MainPage(isChooseFirm: true),
),
);
}
},
);
} else {
_isShowCheckLogin = false;
}
}
}
void _onScroll() {
final offset = _scrollController.hasClients ? _scrollController.offset : 0.0;
final shouldShow = offset >= _triggerOffset;
if (shouldShow != _showFloatingAppBar) {
setState(() => _showFloatingAppBar = shouldShow);
}
}
@override
Future<void> onVisible() async {
final current = CurrentTabNotifier.of(context)?.currentIndex ?? 0;
const myIndex = 0;
if (current != myIndex) return;
// 可在此刷新角标等
}
Future<void> _onRefresh() async {
await Future.delayed(const Duration(seconds: 1));
}
@override
Widget build(BuildContext context) {
const double notificationHeightConst = 60.0;
double bannerHeight = 738 / 1125 * screenWidth(context);
// 计算可见按钮列表
final visibleButtons = <Map<String, dynamic>>[];
for (int i = 0; i < buttonInfos.length; i++) {
if (i < _buttonVisibility.length && _buttonVisibility[i]) {
visibleButtons.add(buttonInfos[i]);
}
}
final double iconSectionHeight =
visibleButtons.isEmpty ? 0.0 : (visibleButtons.length <= 4 ? 120.0 : 220.0);
final double notificationHeight =
(_showNotificationBar && iconSectionHeight >= 0) ? notificationHeightConst : 0.0;
final double extraSpacing = _showNotificationBar ? 60.0 : 12.0;
final double stackBottom =
bannerHeight - (iconSectionHeight * 0.4) + iconSectionHeight + extraSpacing;
final double statusBar = MediaQuery.of(context).padding.top;
final topPadding = Platform.isIOS ? 0 : 14;
return PopScope(
canPop: false,
child: Scaffold(
extendBodyBehindAppBar: true,
body: Stack(
children: [
RefreshIndicator(
backgroundColor: Colors.white,
color: Colors.blue,
onRefresh: _onRefresh,
child: ListView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(0),
children: [
SizedBox(
height: stackBottom,
child: Stack(
clipBehavior: Clip.none,
children: [
// Banner
Positioned(
top: 0,
left: 0,
right: 0,
height: bannerHeight,
child: _buildBannerSection(bannerHeight),
),
// 通知栏
if (_showNotificationBar)
Positioned(
left: 10,
right: 10,
top: (bannerHeight - (iconSectionHeight * 0.4)) + iconSectionHeight - 10,
height: notificationHeight,
child: _buildNotificationBar(notificationHeight - 2),
),
// 图标区
Positioned(
left: 10,
right: 10,
top: bannerHeight - (iconSectionHeight * 0.4),
height: iconSectionHeight,
child: _buildIconSection(context, visibleButtons, iconSectionHeight),
),
],
),
),
if (widget.isChooseFirm && _showWorkStats) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: _buildWorkStatsSection(),
),
],
if (widget.isChooseFirm && _showCheckList) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: _buildCheckListSection(),
),
const SizedBox(height: 20),
],
],
),
),
// 浮动 AppBar
Positioned(
top: 0,
left: 0,
right: 0,
child: AnimatedContainer(
duration: const Duration(milliseconds: 0),
height: _showFloatingAppBar ? (statusBar + _floatingBarHeight) : 0,
decoration: BoxDecoration(
color: _showFloatingAppBar ? h_AppBarColor() : Colors.transparent,
boxShadow: _showFloatingAppBar
? [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 6)]
: [],
),
child: SafeArea(
bottom: false,
child: Opacity(
opacity: _showFloatingAppBar ? 1.0 : 0.0,
child: SizedBox(height: _floatingBarHeight, child: const SizedBox()),
),
),
),
),
// 顶部中央标题
Positioned(
top: statusBar + topPadding,
width: screenWidth(context),
child: Center(
child: Text(
'秦港-相关方安全管理',
style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Colors.white),
),
),
),
// 右上角图标(扫码、加入企业)
_buildFixedTopIcons(context),
],
),
),
);
}
Future<void> _joinFirm() async {
pushPage(FirmListPage(isBack: true), context);
}
Widget _buildFixedTopIcons(BuildContext context) {
final double statusBar = MediaQuery.of(context).padding.top;
final double topOffset = statusBar + 12;
final routeService = RouteService();
final bool showScan = routeService.hasPerm(_modulePerms['scan']!);
final bool showJoin = routeService.hasPerm(_modulePerms['joinFirm']!);
final List<Widget> children = [];
if (showScan) {
children.add(
GestureDetector(
onTap: startScan,
child: Container(
width: 30,
height: 30,
alignment: Alignment.center,
child: Image.asset("assets/icon-apps/home_saoyisao.png", width: 20, height: 20),
),
),
);
}
if (showJoin) {
if (children.isNotEmpty) children.add(const SizedBox(width: 8));
children.add(
GestureDetector(
onTap: _joinFirm,
child: Container(
width: 30,
height: 30,
alignment: Alignment.center,
child: Image.asset("assets/icon-apps/home_add.png", width: 20, height: 20),
),
),
);
}
return Positioned(
top: topOffset,
right: 8,
child: Row(children: children),
);
}
Widget _buildNotificationBar(double notificationHeight) {
return Material(
color: Colors.transparent,
child: Container(
height: notificationHeight,
decoration: BoxDecoration(
color: const Color(0xFFE6F5FF),
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.white, width: 1),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 6, offset: const Offset(0, 2)),
],
),
child: Row(
children: [
const SizedBox(width: 12),
Image.asset('assets/images/ico8.png', width: 30, height: 25),
const SizedBox(width: 12),
Expanded(
child: SizedBox(
height: notificationHeight,
child: PageView.builder(
controller: _notifPageController,
scrollDirection: Axis.vertical,
physics: const NeverScrollableScrollPhysics(),
itemCount: _notifications.length,
itemBuilder: (context, index) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(
_notifications[index],
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.black87, fontSize: 14),
),
),
);
},
),
),
),
const Icon(Icons.chevron_right, color: Colors.black26),
const SizedBox(width: 8),
],
),
),
);
}
Widget _buildBannerSection(double bannerHeight) {
return Image.asset(
"assets/images/banner.png",
width: MediaQuery.of(context).size.width,
height: bannerHeight,
fit: BoxFit.cover,
);
}
Widget _buildIconSection(BuildContext context, List<Map<String, dynamic>> visibleButtons, double height) {
if (RouteService().mainTabs.isNotEmpty && visibleButtons.isEmpty) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 5),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))],
),
child: const Center(child: Text('暂无权限访问的功能')),
);
}
final hasSecondRow = visibleButtons.length > 4;
final double containerMinHeight = height > 0 ? height : 120.0;
return Container(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5),
constraints: BoxConstraints(minHeight: containerMinHeight),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))],
),
child: widget.isChooseFirm
? Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: List.generate(4, (i) {
if (i < visibleButtons.length) {
return Expanded(child: Center(child: _buildIconButton(visibleButtons[i], context)));
} else {
return const Expanded(child: SizedBox());
}
}),
),
if (hasSecondRow) const SizedBox(height: 20),
if (hasSecondRow)
Row(
children: List.generate(4, (i) {
final idx = 4 + i;
if (idx < visibleButtons.length) {
return Expanded(child: Center(child: _buildIconButton(visibleButtons[idx], context)));
} else {
return const Expanded(child: SizedBox());
}
}),
),
],
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/ico1.png', width: 100, height: 100),
const SizedBox(height: 10),
CustomButton(
text: '点击入职企业',
onPressed: () => pushPage(FirmListPage(isBack: true), context),
),
],
),
),
);
}
Widget _buildIconButton(Map<String, dynamic> info, BuildContext context) {
return GestureDetector(
onTap: () => _handleIconTap(info['title']),
child: Column(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFFE8F4FD),
borderRadius: BorderRadius.circular(25),
),
child: Center(child: Image.asset(info['icon'], width: 30, height: 30)),
),
const SizedBox(height: 6),
SizedBox(
width: 70,
child: Text(
info['title'],
style: const TextStyle(fontSize: 12, color: Colors.black87),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
void _handleIconTap(String title) {
switch (title) {
case "单位管理":
pushPage(UnitTabPage(), context);
break;
case "入港培训":
pushPage(StudyTabListPage(), context);
break;
default:
ToastUtil.showNormal(context, '功能开发中...');
break;
}
}
Widget _buildWorkStatsSection() {
double buttonHeight = 45.0;
return Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: screenWidth(context),
height: buttonHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => _isMobileSelected = true),
child: Container(
decoration: BoxDecoration(
color: _isMobileSelected ? const Color(0xFFE9F4FE) : Colors.white,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(12)),
),
child: Center(
child: Text(
"手机端",
style: TextStyle(
fontSize: 14,
color: _isMobileSelected ? Colors.black : Colors.blue,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
Image.asset(
'assets/images/${_isMobileSelected ? 'img2.png' : 'img1.png'}',
fit: BoxFit.cover,
height: buttonHeight,
width: 30,
),
Expanded(
child: GestureDetector(
onTap: () => setState(() => _isMobileSelected = false),
child: Container(
decoration: BoxDecoration(
color: !_isMobileSelected ? const Color(0xFFE9F4FE) : Colors.white,
borderRadius: const BorderRadius.only(topRight: Radius.circular(12)),
),
child: Center(
child: Text(
"电脑端",
style: TextStyle(
fontSize: 15,
color: !_isMobileSelected ? Colors.black : Colors.blue,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(15),
child: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 0),
child: RichText(
text: TextSpan(
children: [
const TextSpan(text: "今日有工作项", style: TextStyle(fontSize: 16, color: Colors.black87)),
TextSpan(
text: " ${workStats['total']}",
style: const TextStyle(fontSize: 16, color: Color(0xFF2A75F8), fontWeight: FontWeight.bold),
),
const TextSpan(text: "", style: TextStyle(fontSize: 16, color: Colors.black87)),
],
),
),
),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RichText(
text: TextSpan(
children: [
const TextSpan(text: "已处理工作项:", style: TextStyle(fontSize: 14, color: Colors.black87)),
TextSpan(
text: " ${workStats['processed']}",
style: const TextStyle(fontSize: 14, color: Colors.greenAccent, fontWeight: FontWeight.bold),
),
const TextSpan(text: "", style: TextStyle(fontSize: 14, color: Colors.black87)),
],
),
),
RichText(
text: TextSpan(
children: [
const TextSpan(text: "待处理工作项:", style: TextStyle(fontSize: 14, color: Colors.black87)),
TextSpan(
text: " ${workStats['pending']}",
style: const TextStyle(fontSize: 14, color: Colors.orange, fontWeight: FontWeight.bold),
),
const TextSpan(text: "", style: TextStyle(fontSize: 14, color: Colors.black87)),
],
),
),
],
),
],
),
),
],
),
);
}
Widget _buildCheckListSection() {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(' 待办清单', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(),
],
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))],
),
child: Column(children: [...checkLists.map((item) => _buildCheckListItem(item))]),
),
],
);
}
Widget _buildCheckListItem(Map<String, dynamic> item) {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey[200]!, width: 1)),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
item['title'],
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.black87),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
const SizedBox(width: 8),
Text(item['type'], style: TextStyle(fontSize: 12, color: Colors.grey[500])),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(4),
),
child: Text(
"类型:${item['type']}",
style: const TextStyle(fontSize: 12, color: Colors.blue),
overflow: TextOverflow.ellipsis,
),
),
),
const SizedBox(width: 8),
Flexible(
child: Text(
"时间:${item['time']}",
style: const TextStyle(fontSize: 12, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
],
),
);
}
}