From 37651441b25dcdc1f6673ac898d430bd2e5e32cf Mon Sep 17 00:00:00 2001 From: xufei <727302827@qq.com> Date: Sat, 28 Feb 2026 16:18:08 +0800 Subject: [PATCH] =?UTF-8?q?2026.2.28=20=E6=B6=88=E6=81=AF=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/icon-apps/read_message.png | Bin 0 -> 5970 bytes assets/icon-apps/unread_message.png | Bin 0 -> 5208 bytes lib/http/modules/notif_api.dart | 69 ++++ lib/pages/badge_manager.dart | 129 +++++++ lib/pages/main_tab.dart | 47 ++- lib/pages/notif/notif_detail_page.dart | 189 ++++++++++ lib/pages/notif/notif_page.dart | 497 +++++++++++++++++++++++++ 7 files changed, 927 insertions(+), 4 deletions(-) create mode 100644 assets/icon-apps/read_message.png create mode 100644 assets/icon-apps/unread_message.png create mode 100644 lib/http/modules/notif_api.dart create mode 100644 lib/pages/badge_manager.dart create mode 100644 lib/pages/notif/notif_detail_page.dart create mode 100644 lib/pages/notif/notif_page.dart diff --git a/assets/icon-apps/read_message.png b/assets/icon-apps/read_message.png new file mode 100644 index 0000000000000000000000000000000000000000..aaa902ad4c7ba41441ba420d100b6619f5bde078 GIT binary patch literal 5970 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

5^Rbx)lgOmg_>zs-RFKIcl*n3DwvVInqWYq|Y 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 getNoticeList(Map data) { + return HttpManager().request( + '${ApiService.basePath}/appmenu', + '/noticeReadRecord/list', + method: Method.post, + data: { + ...data , + }, + ); + } + + /// 通知详情 + static Future> getNoticeDetail(String noticeId) { + return HttpManager().request( + '${ApiService.basePath}/appmenu', + '/notice/noticeContentInfo/$noticeId', + method: Method.get, + data: { + // ...data , + }, + ); + } + + /// 已阅 + static Future> saveReadNoticeDetail(String noticeId) { + return HttpManager().request( + '${ApiService.basePath}/appmenu', + '/noticeReadRecord/save', + method: Method.post, + data: { + "noticeId": noticeId, + }, + ); + } + + /// 回复 + static Future> replyContent(String noticeReadId,String replyContent) { + return HttpManager().request( + '${ApiService.basePath}/appmenu', + '/noticeReadRecord/reply', + method: Method.put, + data: { + "noticeReadId": noticeReadId, + "replyContent": replyContent, + }, + ); + } + + /// 通知数量 + static Future> getNotifRedPoint() { + return HttpManager().request( + '${ApiService.basePath}/appmenu', + '/notice/unreadCount', + method: Method.get, + data: { + // ...data , + }, + ); + } + + +} \ No newline at end of file diff --git a/lib/pages/badge_manager.dart b/lib/pages/badge_manager.dart new file mode 100644 index 0000000..3ba68d5 --- /dev/null +++ b/lib/pages/badge_manager.dart @@ -0,0 +1,129 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_new_badger/flutter_new_badger.dart'; +import 'package:qhd_prevention/common/route_service.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/http/modules/notif_api.dart'; + + +/// 优化版 BadgeManager:每个接口超时保护、增量更新、notify 合并、原生角标 debounce +class BadgeManager extends ChangeNotifier { + BadgeManager._internal(); + static final BadgeManager _instance = BadgeManager._internal(); + factory BadgeManager() => _instance; +// 添加完整的工作数据存储 + Map _workData = {}; + Map get workData => _workData; + // 各模块未读 + int _notifCount = 0; + + + // 读取接口值的公开 getter + int get count => _notifCount ; + int get notifCount => _notifCount; + + + + // 通知合并控制(短时间内多次更新只触发一次 notify) + Timer? _notifyTimer; + Duration _notifyDelay = const Duration(milliseconds: 180); + void _scheduleNotify() { + // 如果已有计划,不需要再立即计划(合并) + _notifyTimer?.cancel(); + _notifyTimer = Timer(_notifyDelay, () { + try { + notifyListeners(); + } catch (e) { + debugPrint('BadgeManager.notifyListeners error: $e'); + } + _notifyTimer = null; + }); + } + + // 原生角标同步 debounce(防止短时间内频繁调用) + Timer? _syncTimer; + Duration _syncDelay = const Duration(milliseconds: 250); + void _syncNativeDebounced() { + _syncTimer?.cancel(); + _syncTimer = Timer(_syncDelay, () { + try { + final total = count; + if (total > 0) { + FlutterNewBadger.setBadge(total); + } else { + FlutterNewBadger.removeBadge(); + } + } catch (e) { + debugPrint('BadgeManager._syncNativeDebounced error: $e'); + } + _syncTimer = null; + }); + } + + // safe wrapper: 给单个 future 加超时与兜底返回 + Future _safe(Future future, T fallback, {Duration timeout = const Duration(seconds: 5)}) async { + try { + return await future.timeout(timeout); + } catch (e, st) { + // 报错不抛出到外层,记录并返回 fallback + debugPrint('BadgeManager._safe error: $e\n$st'); + return fallback; + } + } + + /// 初始化所有模块(并行发起,但每个接口独立 timeout & 提交结果) + Future initAllModules() async { + // 不 await 整个 Future.wait,使调用方不会因为单个慢接口阻塞 + try { + // 每个请求都通过 _safe 包裹,设置超时与兜底 + final fNotif = _safe>( + NotifApi.getNotifRedPoint().then((r) => r as Map), + {}, + timeout: const Duration(seconds: 4), + ); + + + fNotif.then((notifJson) { + try { + _notifCount = ((notifJson['data'] as int?) ?? 0); + } catch (e, st) { + debugPrint('BadgeManager.parse notifJson error: $e\n$st'); + } + _scheduleNotify(); + _syncNativeDebounced(); + }); + + + } catch (e, st) { + debugPrint('BadgeManager.initAllModules unexpected error: $e\n$st'); + } + } + + // 下面的 updateX 方法也做了超时保护并使用 _onModuleChanged 合并通知 + + void updateNotifCount() async { + try { + final notifJson = await _safe>( + NotifApi.getNotifRedPoint().then((r) => r as Map), + {}, + timeout: const Duration(seconds: 4), + ); + _notifCount = (notifJson['data'] as int?) ?? 0; + _onModuleChanged(); + } catch (e) { + debugPrint('updateNotifCount error: $e'); + } + } + + + void clearAll() { + _syncNativeDebounced(); + _scheduleNotify(); + } + + void _onModuleChanged() { + _syncNativeDebounced(); + _scheduleNotify(); + } +} diff --git a/lib/pages/main_tab.dart b/lib/pages/main_tab.dart index 121197c..319d891 100644 --- a/lib/pages/main_tab.dart +++ b/lib/pages/main_tab.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:qhd_prevention/pages/badge_manager.dart'; import 'package:qhd_prevention/pages/home/home_page.dart'; import 'package:qhd_prevention/pages/mine/mine_page.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/pages/notif/notif_page.dart'; import 'package:qhd_prevention/services/heartbeat_service.dart'; /// 用于向子树公布当前 tab 索引 @@ -35,21 +37,33 @@ class MainPage extends StatefulWidget { class _MainPageState extends State with WidgetsBindingObserver { int _currentIndex = 0; final GlobalKey _homeKey = GlobalKey(); - + final GlobalKey _notifKey = GlobalKey(); // final GlobalKey _appKey = GlobalKey(); final GlobalKey _mineKey = GlobalKey(); late List _pages; late List _tabVisibility; // 存储每个Tab的显示状态 + // 添加 BadgeManager 实例 + late final BadgeManager _badgeManager; + @override void initState() { super.initState(); + + // 初始化 BadgeManager + _badgeManager = BadgeManager(); + _badgeManager.initAllModules(); + + // 监听 BadgeManager 的变化 + _badgeManager.addListener(_onBadgeChanged); + // 注册生命周期监听 WidgetsBinding.instance.addObserver(this); // 初始化所有Tab _tabVisibility = [true, true, true]; _pages = [ HomePage(key: _homeKey, isChooseFirm: widget.isChooseFirm), + NotifPage(key: _notifKey), MinePage(key: _mineKey, isChooseFirm: widget.isChooseFirm), ]; // 启动心跳服务 @@ -58,7 +72,7 @@ class _MainPageState extends State with WidgetsBindingObserver { @override void dispose() { - // BadgeManager().removeListener(_onBadgeChanged); + _badgeManager.removeListener(_onBadgeChanged); // 移除生命周期监听 WidgetsBinding.instance.removeObserver(this); @@ -122,7 +136,7 @@ class _MainPageState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { - // final bm = BadgeManager(); + final bm = _badgeManager; // 构建可见的底部导航项 final List visibleItems = []; @@ -150,6 +164,20 @@ class _MainPageState extends State with WidgetsBindingObserver { visiblePages.add(_pages[i]); break; case 1: + visibleItems.add(BottomNavigationBarItem( + icon: _buildIconWithBadge( + icon: Image.asset('assets/tabbar/works.png', width: 24, height: 24), + badgeCount: bm.notifCount, + ), + activeIcon: _buildIconWithBadge( + icon: Image.asset('assets/tabbar/works_cur.png', width: 24, height: 24), + badgeCount: bm.notifCount, + ), + label: '通知', + )); + visiblePages.add(_pages[i]); + break; + case 2: visibleItems.add( BottomNavigationBarItem( icon: Image.asset( @@ -167,7 +195,6 @@ class _MainPageState extends State with WidgetsBindingObserver { ); visiblePages.add(_pages[i]); break; - break; } } } @@ -218,4 +245,16 @@ class _MainPageState extends State with WidgetsBindingObserver { ), ); } + + + void _onBadgeChanged() { + // 当角标数据变化时,只更新需要重建的部分 + // 但这里我们只需要触发 build 来更新底部导航栏的角标 + if (mounted) { + setState(() { + // 只触发重建,不改变数据 + }); + } + } + } diff --git a/lib/pages/notif/notif_detail_page.dart b/lib/pages/notif/notif_detail_page.dart new file mode 100644 index 0000000..2aea4c5 --- /dev/null +++ b/lib/pages/notif/notif_detail_page.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.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/modules/notif_api.dart'; +import 'package:qhd_prevention/pages/mine/webViewPage.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/tools/h_colors.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import '../../http/ApiService.dart'; + + +class NotifDetailPage extends StatefulWidget { + const NotifDetailPage(this.item, this.selectedTab, {super.key,required this.onClose}); + + final Function(String) onClose; // 回调函数 + final dynamic item; + final int selectedTab; + + @override + State createState() => _NotifDetailPageState(item,selectedTab,onClose); +} + +class _NotifDetailPageState extends State { + _NotifDetailPageState(this.item, this.selectedTab,this.onClose); + + final Function(String) onClose; // 回调函数 + final dynamic item; + final int selectedTab; + String title=""; + String time=""; + String text=""; + bool isShowReply=false; + String replyText=""; + String noticeReadId=''; + + @override + void initState() { + super.initState(); + if(0==selectedTab){ + _getNotifDetail(); + }else{ + _getNotifEnterpriseDetail(); + } + + } + + Future _getNotifDetail() async { + // LoadingDialogHelper.show(); + try { + final result = await NotifApi.getNoticeDetail(item['noticeId']); + if (result['success']) { + final dynamic newList = result['data'] ; + setState(() { + title= newList['title']??''; + time= newList['publishTime']??''; + text= newList['content']??''; + isShowReply=newList['requireReply']==1?true:false; + noticeReadId=newList['noticeReadId']??''; + replyText=newList['replyContent']??''; + }); + if(newList['readStatus']!='1'){ + final resultTwo = await NotifApi.saveReadNoticeDetail(item['noticeId']); + if (result['success']) { + noticeReadId = resultTwo['data'] ?? ''; + } + } + } + } catch (e) { + print('加载出错: $e'); + } + } + + Future _getNotifEnterpriseDetail() async { + // LoadingDialogHelper.show(); + try { + // final result = await HiddenDangerApi.getNotifEnterpriseDetail(item['NOTICECORPUSERID_ID']); + // if (result['result'] == 'success') { + // final dynamic newList = result['pd'] ; + // setState(() { + // title= newList['SYNOPSIS']; + // time= newList['CREATTIME']; + // text= newList['CONTENT']; + // }); + // } + } catch (e) { + print('加载出错: $e'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar(title: "通知详情"), + body: SafeArea( + child: Container( + // color: Colors.white, + child: Padding( + padding: EdgeInsets.all(10), + child: Card( + child: Container( + width: double.infinity, // 铺满父容器 + color: Colors.white, + padding: const EdgeInsets.all(15), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, // 左对齐 + children: [ + Padding( + padding: EdgeInsets.only(left: 10,right: 10), + child: Text(title,style: TextStyle(color: Colors.black,fontSize: 16,fontWeight: FontWeight.bold),), + ), + + SizedBox(height: 8), + Padding( + padding: EdgeInsets.only(left: 10,right: 10), + child: Text(time,), + ), + SizedBox(height: 8), + Html( + data: text, + ), + + SizedBox(height: 8), + if(isShowReply&&replyText.isEmpty) + CustomButton( + height: 35, + margin: EdgeInsets.only(right: 10), + onPressed: () async { + final confirmed = await CustomAlertDialog.showInput( + context, + title: "回复", + hintText: '输入内容', + cancelText: '关闭', + confirmText: '确定', + ); + if (confirmed != null&&confirmed.isNotEmpty) { + //确定回复 + // ToastUtil.showNormal(context, confirmed); + _replyContent(confirmed); + } + }, + backgroundColor: h_AppBarColor(), + textStyle: const TextStyle(color: Colors.white), + buttonStyle: ButtonStyleType.primary, + text: '回复', + ), + ], + ), + ), + + ), + ), + + ), + ), + ), + ); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + onClose('关闭详情'); // 触发回调 + } + + + Future _replyContent(String content) async { + try { + LoadingDialogHelper.show(); + final result = await NotifApi.replyContent(noticeReadId,content); + LoadingDialogHelper.hide(); + if (result['success']) { + ToastUtil.showNormal(context, '回复成功'); + Navigator.pop(context); + // onClose('关闭详情'); // 触发回调 + } + } catch (e) { + print('加载出错: $e'); + } + } + + +} diff --git a/lib/pages/notif/notif_page.dart b/lib/pages/notif/notif_page.dart new file mode 100644 index 0000000..3eecd37 --- /dev/null +++ b/lib/pages/notif/notif_page.dart @@ -0,0 +1,497 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.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/search_bar_widget.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/http/modules/notif_api.dart'; +import 'package:qhd_prevention/pages/badge_manager.dart'; +import 'package:qhd_prevention/pages/main_tab.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; + +import 'package:qhd_prevention/pages/notif/notif_detail_page.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import '../../http/ApiService.dart'; + +class NotifPage extends StatefulWidget { + const NotifPage({Key? key}) : super(key: key); + + @override + NotifPageState createState() => NotifPageState(); +} + +class NotifPageState extends RouteAwareState + with TickerProviderStateMixin, WidgetsBindingObserver { + final TextEditingController searchController = TextEditingController(); + + late List _list = []; + + + late TabController _tabController; + late TabController _tabControllerTwo; + int _selectedTab = 0; + int _selectedTabTwo = 0; + int pageNum = 1; + int _totalPage=1; + String keyWord = ""; + + // 控制Tab显示状态 + bool _showPlatformAnnouncement = true; + bool _showEnterpriseAnnouncement = true; + + // 标记是否已初始化 + bool _isInitialized = false; + bool _isLoading = false; + bool _hasMore = true; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + + // 初始化显示状态 + _initializeVisibility(); + + // 初始化TabController + _initializeTabControllers(); + + _getListData(false); + + _isInitialized = true; + } + + void _initializeVisibility() { + _showPlatformAnnouncement = true; + _showEnterpriseAnnouncement = true; + } + + void _initializeTabControllers() { + final visibleTabsCount = + (_showPlatformAnnouncement ? 1 : 0) + + (_showEnterpriseAnnouncement ? 1 : 0); + + _tabController = TabController(length: visibleTabsCount, vsync: this); + _tabController.addListener(() { + if (_tabController.indexIsChanging) { + setState(() => _selectedTab = _tabController.index); + print('切换到标签:${_tabController.index}'); + searchController.text = ""; + keyWord = ""; + reRefreshData(); + } + }); + + _tabControllerTwo = TabController(length: 2, vsync: this); + _tabControllerTwo.addListener(() { + if (_tabControllerTwo.indexIsChanging) { + setState(() => _selectedTabTwo = _tabControllerTwo.index); + print('切换到标签:${_tabControllerTwo.index}'); + + searchController.text = ""; + keyWord = ""; + reRefreshData(); + } + }); + } + + void _updateNotifVisibility() { + final routeService = RouteService(); + final notifRoutes = + routeService.mainTabs.isNotEmpty + ? routeService.getRoutesForTab(routeService.mainTabs[2]) + : []; + + bool newPlatformAnnouncement = false; + bool newEnterpriseAnnouncement = false; + + // 根据路由标题匹配并设置显示状态 + for (final route in notifRoutes) { + final routeTitle = route.title; + + switch (routeTitle) { + case '公告': + newPlatformAnnouncement = route.hasMenu; + break; + case '通知': + newEnterpriseAnnouncement = route.hasMenu; + break; + } + } + + // 只有当显示状态确实发生变化时才更新 + if (newPlatformAnnouncement != _showPlatformAnnouncement || + newEnterpriseAnnouncement != _showEnterpriseAnnouncement) { + setState(() { + _showPlatformAnnouncement = newPlatformAnnouncement; + _showEnterpriseAnnouncement = newEnterpriseAnnouncement; + + // 重新初始化TabController + _disposeTabControllers(); + _initializeTabControllers(); + + // 重置选中状态 + _selectedTab = 0; + _selectedTabTwo = 0; + }); + } + } + + void _disposeTabControllers() { + _tabController.dispose(); + _tabControllerTwo.dispose(); + } + + void onRouteConfigLoaded() { + if (mounted && _isInitialized) { + _updateNotifVisibility(); + } + } + + void reRefreshData() { + pageNum = 1; + _list.clear(); + _getListData(false); + // if (0 == _selectedTab) { + // + // _getNotifList(); + // } else { + // _getNotifEnterprise(); + // } + } + + @override + Future onVisible() async { + final current = CurrentTabNotifier.of(context)?.currentIndex ?? 0; + const myIndex = 2; + if (current != myIndex) { + return; + } + // BadgeManager().updateNotifCount(); + } + + Future _getListData(bool loadMore) async { + try { + if (_isLoading) return; + _isLoading = true; + + LoadingDialogHelper.show(); + + final Map result; + + final data = { + "pageSize": 20, + "pageIndex": pageNum, + }; + + if(_selectedTab==0){ + result = await NotifApi.getNoticeList(data); + }else{ + result = await NotifApi.getNoticeList(data); + } + BadgeManager().updateNotifCount(); + LoadingDialogHelper.hide(); + if(_selectedTab==0){ + if (result['success']) { + _totalPage =result['totalPages'] ?? 1; + final List newList = result['data'] ?? []; + // setState(() { + // _list.addAll(newList); + // }); + + setState(() { + if (loadMore) { + _list.addAll(newList); + } else { + _list = newList; + } + _hasMore = pageNum < _totalPage; + // if (_hasMore) _page++; + }); + + }else{ + ToastUtil.showNormal(context, "加载数据失败"); + // _showMessage('加载数据失败'); + } + } + + + } catch (e) { + LoadingDialogHelper.hide(); + // 出错时可以 Toast 或者在页面上显示错误状态 + print('加载数据失败:$e'); + } finally { + // if (!loadMore) LoadingDialogHelper.hide(); + _isLoading = false; + } + } + + + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _disposeTabControllers(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // 构建可见的Tab列表 + final List visibleTabs = []; + if (_showPlatformAnnouncement) { + visibleTabs.add(const Tab(text: '公告')); + } + if (_showEnterpriseAnnouncement) { + visibleTabs.add(const Tab(text: '通知')); + } + + // 如果没有可见的Tab,显示空页面 + if (visibleTabs.isEmpty) { + return Scaffold( + appBar: MyAppbar(title: '通知', isBack: false), + body: Center( + child: Text( + '暂无通知权限', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ), + ); + } + + return Scaffold( + appBar: MyAppbar(title: '通知', isBack: false), + body: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + behavior: HitTestBehavior.opaque, + child: Scaffold( + body: SafeArea( + child: Column( + children: [ + // Tab bar + Container( + color: Colors.white, + child: TabBar( + controller: _tabController, + labelStyle: const TextStyle(fontSize: 16), + indicator: const UnderlineTabIndicator( + borderSide: BorderSide( + width: 4.0, + color: const Color(0xFF1C61FF), + ), + insets: EdgeInsets.symmetric(horizontal: 35.0), + ), + labelColor: const Color(0xFF1C61FF), + unselectedLabelColor: Colors.grey, + tabs: visibleTabs, + ), + ), + + NotificationListener( + onNotification: _onScroll, + child: // List + Expanded( + child: + _list.isEmpty + ? NoDataWidget.show() + : ListView.builder( + itemCount: _list.length, + itemBuilder: (context, index) { + return _itemCellTwo(_list[index]); + }, + ), + ), + ), + + + ], + ), + ), + ), + ), + ); + } + + bool _onScroll(ScrollNotification n) { + if (n.metrics.pixels > n.metrics.maxScrollExtent - 100 && + _hasMore && + !_isLoading) { + pageNum++; + _getListData(true); + } + return false; + } + + Widget _itemCellTwo(final item) { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: + (context) => NotifDetailPage( + item, + _selectedTab, + onClose: (result) { + print('详情页面已关闭,返回结果: $result'); + reRefreshData(); + }, + ), + ), + ); + }, + child: Card( + margin: EdgeInsets.all(8.0), + color: Colors.white, + elevation: 2.0, + child: Padding( + padding: EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题 + Text( + item['title']??'', + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + + SizedBox(height: 8.0), + + Text( + '发布时间:${item['publishTime']??''}', + style: TextStyle(fontSize: 14.0, color: Colors.grey[500]), + ), + ], + ), + ), + + SizedBox(height: 8.0), + + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + item['readStatus']=='1'?'已阅':'待阅', + style: TextStyle( + fontSize: 16.0, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 6.0), + Image.asset( + item['readStatus']=='1'?'assets/icon-apps/read_message.png':'assets/icon-apps/unread_message.png', + width: 25, + height: 25, + ), + const SizedBox(width: 6.0), + ], + ), + ], + ), + ), + ), + ); + } + + + + + + + Widget _itemCell(final item) { + return Column( + children: [ + ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: + (context) => NotifDetailPage( + item, + _selectedTab, + onClose: (result) { + print('详情页面已关闭,返回结果: $result'); + reRefreshData(); + }, + ), + ), + ); + }, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + title: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text(item['SYNOPSIS'], style: const TextStyle(fontSize: 14)), + ), + subtitle: Text( + item['CREATTIME'], + style: const TextStyle(fontSize: 13), + ), + trailing: Container( + constraints: const BoxConstraints(minHeight: 100), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (0 != _selectedTab) + Text( + item['TYPE'] == 1 ? '已读' : '未读', + style: TextStyle( + fontSize: 12, + color: item['TYPE'] == 1 ? Colors.grey : Colors.red, + ), + ), + const SizedBox(height: 15), + if (0 != _selectedTab && item['TYPE'] == 1) + SizedBox( + height: 24, + child: TextButton( + onPressed: () async { + final ok = await CustomAlertDialog.showConfirm( + context, + title: '确认删除', + content: '确定要删除这条通知吗?', + cancelText: '取消', + ); + // if (ok) { + // _deleteNotif(item['NOTICECORPUSERID_ID']); + // } + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12), + backgroundColor: Colors.red, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + '删除', + style: TextStyle(fontSize: 13, color: Colors.white), + ), + ), + ), + ], + ), + ), + ), + const Divider(height: 1, color: Colors.black12), + ], + ); + } +}