2026.2.28 消息通知

master
xufei 2026-02-28 16:18:08 +08:00
parent e9644fea24
commit 37651441b2
7 changed files with 927 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1,69 @@
import 'package:dio/dio.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/http/HttpManager.dart';
class NotifApi {
///
static Future<Map<String, dynamic>> getNoticeList(Map data) {
return HttpManager().request(
'${ApiService.basePath}/appmenu',
'/noticeReadRecord/list',
method: Method.post,
data: {
...data ,
},
);
}
///
static Future<Map<String, dynamic>> getNoticeDetail(String noticeId) {
return HttpManager().request(
'${ApiService.basePath}/appmenu',
'/notice/noticeContentInfo/$noticeId',
method: Method.get,
data: {
// ...data ,
},
);
}
///
static Future<Map<String, dynamic>> saveReadNoticeDetail(String noticeId) {
return HttpManager().request(
'${ApiService.basePath}/appmenu',
'/noticeReadRecord/save',
method: Method.post,
data: {
"noticeId": noticeId,
},
);
}
///
static Future<Map<String, dynamic>> replyContent(String noticeReadId,String replyContent) {
return HttpManager().request(
'${ApiService.basePath}/appmenu',
'/noticeReadRecord/reply',
method: Method.put,
data: {
"noticeReadId": noticeReadId,
"replyContent": replyContent,
},
);
}
///
static Future<Map<String, dynamic>> getNotifRedPoint() {
return HttpManager().request(
'${ApiService.basePath}/appmenu',
'/notice/unreadCount',
method: Method.get,
data: {
// ...data ,
},
);
}
}

View File

@ -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';
/// BadgeManagernotify debounce
class BadgeManager extends ChangeNotifier {
BadgeManager._internal();
static final BadgeManager _instance = BadgeManager._internal();
factory BadgeManager() => _instance;
//
Map<String, dynamic> _workData = {};
Map<String, dynamic> 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<T> _safe<T>(Future<T> 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<void> initAllModules() async {
// await Future.wait使
try {
// _safe
final fNotif = _safe<Map<String, dynamic>>(
NotifApi.getNotifRedPoint().then((r) => r as Map<String, dynamic>),
<String, dynamic>{},
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<Map<String, dynamic>>(
NotifApi.getNotifRedPoint().then((r) => r as Map<String, dynamic>),
<String, dynamic>{},
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();
}
}

View File

@ -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<MainPage> with WidgetsBindingObserver {
int _currentIndex = 0;
final GlobalKey<HomePageState> _homeKey = GlobalKey<HomePageState>();
final GlobalKey<NotifPageState> _notifKey = GlobalKey<NotifPageState>();
// final GlobalKey<ApplicationPageTwoState> _appKey = GlobalKey<ApplicationPageTwoState>();
final GlobalKey<MinePageState> _mineKey = GlobalKey<MinePageState>();
late List<Widget> _pages;
late List<bool> _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 = <Widget>[
HomePage(key: _homeKey, isChooseFirm: widget.isChooseFirm),
NotifPage(key: _notifKey),
MinePage(key: _mineKey, isChooseFirm: widget.isChooseFirm),
];
//
@ -58,7 +72,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
@override
void dispose() {
// BadgeManager().removeListener(_onBadgeChanged);
_badgeManager.removeListener(_onBadgeChanged);
//
WidgetsBinding.instance.removeObserver(this);
@ -122,7 +136,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
// final bm = BadgeManager();
final bm = _badgeManager;
//
final List<BottomNavigationBarItem> visibleItems = [];
@ -150,6 +164,20 @@ class _MainPageState extends State<MainPage> 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<MainPage> with WidgetsBindingObserver {
);
visiblePages.add(_pages[i]);
break;
break;
}
}
}
@ -218,4 +245,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
),
);
}
void _onBadgeChanged() {
//
// build
if (mounted) {
setState(() {
//
});
}
}
}

View File

@ -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<NotifDetailPage> createState() => _NotifDetailPageState(item,selectedTab,onClose);
}
class _NotifDetailPageState extends State<NotifDetailPage> {
_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<void> _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<void> _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<void> _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');
}
}
}

View File

@ -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<NotifPage>
with TickerProviderStateMixin, WidgetsBindingObserver {
final TextEditingController searchController = TextEditingController();
late List<dynamic> _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<void> onVisible() async {
final current = CurrentTabNotifier.of(context)?.currentIndex ?? 0;
const myIndex = 2;
if (current != myIndex) {
return;
}
// BadgeManager().updateNotifCount();
}
Future<void> _getListData(bool loadMore) async {
try {
if (_isLoading) return;
_isLoading = true;
LoadingDialogHelper.show();
final Map<String, dynamic> 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<dynamic> 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<Widget> 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<ScrollNotification>(
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),
],
);
}
}