// 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 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 _buttonVisibility = []; // 页面模块可见性(由路由驱动) bool _showNotificationBar = true; bool _showWorkStats = true; bool _showCheckList = true; List totalList = []; Map workStats = {'total': 36, 'processed': 30, 'pending': 6}; List> 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 _notifications = [ "系统通知:今晚20:00 将进行系统维护,请提前保存数据。", "安全提示:施工区请佩戴安全帽并系好安全带。", "公告:本周五集团例会在多功能厅召开,9:00准时开始。", "提醒:请尽快完成隐患整改清单中的待办项。", ]; int _notifIndex = 0; late final PageController _notifPageController; Timer? _notifTimer; // 图标按钮定义(顺序固定) List> 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 _titleToPerm = { "单位管理": "dashboard-Unit-Management", "现场监管": "dashboard-Site-Supervision", "危险作业": "dashboard-Hazardous-Work", "隐患治理": "dashboard-Hazard-Management", "重点作业": "", // 无对应,暂时留空 "口门门禁": "dashboard-Gate-Access-Control", "入港培训": "dashboard-Study-Training", }; // 模块权限映射(基于新菜单配置) final Map _modulePerms = { "notification": "dashboard-roll-notice", "todoStats": "dashboard-todo-sort", "checklist": "dashboard-todo-list", "scan": "dashboard-scan", }; @override void initState() { super.initState(); _isShowCheckLogin = widget.isChooseFirm; // 初始按钮全隐藏,避免闪烁 _buttonVisibility = List.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 newVisibility = List.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 _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 onVisible() async { final current = CurrentTabNotifier.of(context)?.currentIndex ?? 0; const myIndex = 0; if (current != myIndex) return; // 可在此刷新角标等 } Future _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 = >[]; 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 _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 = true; // 加入企业图标始终显示(或根据业务决定) final List 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> 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 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 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, ), ), ], ), ], ), ), ], ), ); } }