Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
ethan
/
appframe
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Commit 5460fac6
authored
2026-06-29 13:54:19 +0800
by
tanghuan
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
消息通知推送处理
1 parent
005587c7
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
190 additions
and
1 deletions
android/app/build.gradle.kts
lib/bloc/web_cubit.dart
lib/config/locator.dart
lib/services/im_service.dart
lib/services/notification_service.dart
lib/ui/pages/reload_page.dart
lib/ui/pages/web_page.dart
lib/utils/login_util.dart
pubspec.yaml
android/app/build.gradle.kts
View file @
5460fac
...
@@ -21,6 +21,8 @@ android {
...
@@ -21,6 +21,8 @@ android {
ndkVersion = flutter.ndkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
compileOptions {
// flutter_local_notifications 插件要求启用核心库脱糖
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
...
@@ -91,6 +93,9 @@ flutter {
...
@@ -91,6 +93,9 @@ flutter {
}
}
dependencies {
dependencies {
// flutter_local_notifications 插件要求启用核心库脱糖
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
// 添加 AppCompat 支持库(PrivacyActivity需要)
// 添加 AppCompat 支持库(PrivacyActivity需要)
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.appcompat:appcompat:1.6.1")
...
...
lib/bloc/web_cubit.dart
View file @
5460fac
...
@@ -57,6 +57,9 @@ class WebState extends Equatable {
...
@@ -57,6 +57,9 @@ class WebState extends Equatable {
final
int
?
userType
;
final
int
?
userType
;
final
String
?
stuId
;
final
String
?
stuId
;
/// 通知点击等场景需要直接打开的 URL
final
String
?
targetUrl
;
/// getOrientationCmd
/// getOrientationCmd
final
bool
orientationCmdFlag
;
final
bool
orientationCmdFlag
;
final
String
orientationCmdMessage
;
final
String
orientationCmdMessage
;
...
@@ -99,6 +102,7 @@ class WebState extends Equatable {
...
@@ -99,6 +102,7 @@ class WebState extends Equatable {
this
.
classCode
,
this
.
classCode
,
this
.
userType
,
this
.
userType
,
this
.
stuId
,
this
.
stuId
,
this
.
targetUrl
,
this
.
orientationCmdFlag
=
false
,
this
.
orientationCmdFlag
=
false
,
this
.
orientationCmdMessage
=
''
,
this
.
orientationCmdMessage
=
''
,
this
.
windowInfoCmdFlag
=
false
,
this
.
windowInfoCmdFlag
=
false
,
...
@@ -131,6 +135,7 @@ class WebState extends Equatable {
...
@@ -131,6 +135,7 @@ class WebState extends Equatable {
String
?
classCode
,
String
?
classCode
,
int
?
userType
,
int
?
userType
,
String
?
stuId
,
String
?
stuId
,
String
?
targetUrl
,
bool
?
orientationCmdFlag
,
bool
?
orientationCmdFlag
,
String
?
orientationCmdMessage
,
String
?
orientationCmdMessage
,
bool
?
windowInfoCmdFlag
,
bool
?
windowInfoCmdFlag
,
...
@@ -162,6 +167,7 @@ class WebState extends Equatable {
...
@@ -162,6 +167,7 @@ class WebState extends Equatable {
classCode:
classCode
??
this
.
classCode
,
classCode:
classCode
??
this
.
classCode
,
userType:
userType
??
this
.
userType
,
userType:
userType
??
this
.
userType
,
stuId:
stuId
??
this
.
stuId
,
stuId:
stuId
??
this
.
stuId
,
targetUrl:
targetUrl
??
this
.
targetUrl
,
orientationCmdFlag:
orientationCmdFlag
??
this
.
orientationCmdFlag
,
orientationCmdFlag:
orientationCmdFlag
??
this
.
orientationCmdFlag
,
orientationCmdMessage:
orientationCmdMessage
??
this
.
orientationCmdMessage
,
orientationCmdMessage:
orientationCmdMessage
??
this
.
orientationCmdMessage
,
windowInfoCmdFlag:
windowInfoCmdFlag
??
this
.
windowInfoCmdFlag
,
windowInfoCmdFlag:
windowInfoCmdFlag
??
this
.
windowInfoCmdFlag
,
...
@@ -195,6 +201,7 @@ class WebState extends Equatable {
...
@@ -195,6 +201,7 @@ class WebState extends Equatable {
classCode
,
classCode
,
userType
,
userType
,
stuId
,
stuId
,
targetUrl
,
orientationCmdFlag
,
orientationCmdFlag
,
orientationCmdMessage
,
orientationCmdMessage
,
windowInfoCmdFlag
,
windowInfoCmdFlag
,
...
@@ -394,6 +401,15 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
...
@@ -394,6 +401,15 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
}
}
void
_loadHtml
()
{
void
_loadHtml
()
{
// 如果通过路由透传了 targetUrl(如通知点击场景),
// 优先在 WebView 中打开该 URL。
final
targetUrl
=
state
.
targetUrl
;
if
(
targetUrl
!=
null
&&
targetUrl
.
isNotEmpty
)
{
final
String
resolvedUrl
=
'
${Constant.localServerUrl}
/index.html#
$targetUrl
'
;
_controller
.
loadRequest
(
Uri
.
parse
(
resolvedUrl
));
return
;
}
var
sharedPreferences
=
getIt
.
get
<
SharedPreferences
>();
var
sharedPreferences
=
getIt
.
get
<
SharedPreferences
>();
var
debug
=
sharedPreferences
.
getInt
(
'debug'
)
??
0
;
var
debug
=
sharedPreferences
.
getInt
(
'debug'
)
??
0
;
// 构造函数中已拦截判断未登录的情况进行了处理,所以这里不再处理未登录的情况
// 构造函数中已拦截判断未登录的情况进行了处理,所以这里不再处理未登录的情况
...
...
lib/config/locator.dart
View file @
5460fac
...
@@ -49,6 +49,7 @@ import 'package:appframe/services/api_service.dart';
...
@@ -49,6 +49,7 @@ import 'package:appframe/services/api_service.dart';
import
'package:appframe/services/dispatcher.dart'
;
import
'package:appframe/services/dispatcher.dart'
;
import
'package:appframe/services/im_service.dart'
;
import
'package:appframe/services/im_service.dart'
;
import
'package:appframe/services/local_server_service.dart'
;
import
'package:appframe/services/local_server_service.dart'
;
import
'package:appframe/services/notification_service.dart'
;
import
'package:appframe/services/player_service.dart'
;
import
'package:appframe/services/player_service.dart'
;
import
'package:appframe/services/recorder_service.dart'
;
import
'package:appframe/services/recorder_service.dart'
;
import
'package:fluwx/fluwx.dart'
;
import
'package:fluwx/fluwx.dart'
;
...
@@ -237,6 +238,9 @@ Future<void> setupLocator() async {
...
@@ -237,6 +238,9 @@ Future<void> setupLocator() async {
instanceName:
"appApiService"
,
instanceName:
"appApiService"
,
);
);
/// 通知服务
getIt
.
registerSingleton
<
NotificationService
>(
NotificationService
());
/// imService
/// imService
getIt
.
registerSingleton
<
ImService
>(
ImService
());
getIt
.
registerSingleton
<
ImService
>(
ImService
());
...
...
lib/services/im_service.dart
View file @
5460fac
This diff is collapsed.
Click to expand it.
lib/services/notification_service.dart
0 → 100644
View file @
5460fac
import
'dart:convert'
;
import
'dart:io'
;
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/config/routes.dart'
;
import
'package:flutter_local_notifications/flutter_local_notifications.dart'
;
import
'package:flutter/material.dart'
;
import
'package:shared_preferences/shared_preferences.dart'
;
class
NotificationService
{
final
FlutterLocalNotificationsPlugin
_plugin
=
FlutterLocalNotificationsPlugin
();
bool
_isInitialized
=
false
;
bool
get
isInitialized
=>
_isInitialized
;
/// 初始化通知插件
Future
<
void
>
initialize
()
async
{
if
(
_isInitialized
)
return
;
const
androidSettings
=
AndroidInitializationSettings
(
'@mipmap/launcher_icon'
);
const
iosSettings
=
DarwinInitializationSettings
(
requestAlertPermission:
true
,
requestBadgePermission:
true
,
requestSoundPermission:
true
,
);
const
settings
=
InitializationSettings
(
android:
androidSettings
,
iOS:
iosSettings
);
final
result
=
await
_plugin
.
initialize
(
settings
,
onDidReceiveNotificationResponse:
_onNotificationResponse
,
);
_isInitialized
=
result
??
false
;
if
(
_isInitialized
)
{
debugPrint
(
'NotificationService 初始化成功'
);
// Android 13+ 需要请求通知权限
if
(
Platform
.
isAndroid
)
{
final
androidPlugin
=
_plugin
.
resolvePlatformSpecificImplementation
<
AndroidFlutterLocalNotificationsPlugin
>();
await
androidPlugin
?.
requestNotificationsPermission
();
}
}
else
{
debugPrint
(
'NotificationService 初始化失败'
);
}
}
/// 处理通知点击(供外部调用,如离线推送点击)
void
handleNotificationClick
(
String
?
payload
)
{
debugPrint
(
'[notification_service] handleNotificationClick payload:
$payload
'
);
// 通过 /reload 中转路由跳转,确保即使当前已在 /web,
// GoRouter 也会完整重建 WebPage 并加载新的 targetUrl。
final
targetUrl
=
_buildTargetUrl
(
payload
);
debugPrint
(
'[notification_service] targetUrl:
$targetUrl
'
);
if
(
targetUrl
!=
null
&&
targetUrl
.
isNotEmpty
)
{
router
.
go
(
'/reload'
,
extra:
{
'targetUrl'
:
targetUrl
},
);
}
else
{
router
.
go
(
'/web'
);
}
}
/// 前台本地通知点击回调处理
void
_onNotificationResponse
(
NotificationResponse
response
)
{
debugPrint
(
'[notification_service] Notification clicked, payload:
${response.payload}
'
);
handleNotificationClick
(
response
.
payload
);
}
/// 解析通知 payload,结合 auth_roles 缓存拼接目标 URL。
/// payload 格式:{"func": "homework", "uniqueCode": "...", "classCode": "..."}
/// 返回示例:/h5/login/pages/applogin?sessionCode=xxx&userCode=xxx&classCode=xxx&userType=2&stuId=xxx&page=homeworkdetail&uniqueCode=xxx
String
?
_buildTargetUrl
(
String
?
payload
)
{
if
(
payload
==
null
||
payload
.
isEmpty
)
return
null
;
try
{
final
data
=
jsonDecode
(
payload
)
as
Map
<
String
,
dynamic
>;
final
uniqueCode
=
data
[
'uniqueCode'
]
as
String
?;
final
classCode
=
data
[
'classCode'
]
as
String
?;
if
(
uniqueCode
==
null
||
uniqueCode
.
isEmpty
||
classCode
==
null
||
classCode
.
isEmpty
)
{
debugPrint
(
'[notification_service] payload 缺少 uniqueCode 或 classCode'
);
return
null
;
}
final
prefs
=
getIt
.
get
<
SharedPreferences
>();
final
rolesJson
=
prefs
.
getString
(
'auth_roles'
)
??
''
;
if
(
rolesJson
.
isEmpty
)
{
debugPrint
(
'[notification_service] auth_roles 缓存为空'
);
return
null
;
}
final
roles
=
jsonDecode
(
rolesJson
)
as
List
<
dynamic
>;
Map
<
String
,
dynamic
>?
matchedRole
;
for
(
final
role
in
roles
)
{
if
(
role
is
Map
<
String
,
dynamic
>
&&
role
[
'classCode'
]?.
toString
()
==
classCode
)
{
matchedRole
=
role
;
break
;
}
}
if
(
matchedRole
==
null
)
{
debugPrint
(
'[notification_service] 未找到与 classCode=
$classCode
匹配的 role'
);
return
null
;
}
final
stuId
=
matchedRole
[
'stuId'
]?.
toString
()
??
''
;
final
sessionCode
=
prefs
.
getString
(
'auth_sessionCode'
)
??
''
;
final
userCode
=
prefs
.
getString
(
'auth_userCode'
)
??
''
;
return
'/h5/login/pages/applogin?'
'sessionCode=
$sessionCode
&'
'userCode=
$userCode
&'
'classCode=
$classCode
&'
'userType=2&'
'stuId=
$stuId
&'
'page=homeworkdetail&'
'uniqueCode=
$uniqueCode
'
;
}
catch
(
e
,
stackTrace
)
{
debugPrint
(
'[notification_service] 解析 payload 失败:
$e
\n
$stackTrace
'
);
return
null
;
}
}
/// 显示手机通知(懒初始化:首次调用自动触发初始化)
Future
<
void
>
showNotification
({
required
int
id
,
required
String
title
,
required
String
body
,
String
?
payload
,
})
async
{
// 懒初始化:首次调用时自动初始化
if
(!
_isInitialized
)
{
await
initialize
();
}
const
androidDetails
=
AndroidNotificationDetails
(
'homework_message_channel'
,
'作业消息通知'
,
channelDescription:
'接收作业消息推送通知'
,
importance:
Importance
.
high
,
priority:
Priority
.
high
,
);
const
iosDetails
=
DarwinNotificationDetails
();
const
details
=
NotificationDetails
(
android:
androidDetails
,
iOS:
iosDetails
);
await
_plugin
.
show
(
id
,
title
,
body
,
details
,
payload:
payload
);
debugPrint
(
'NotificationService showNotification: id=
$id
, title=
$title
, body=
$body
'
);
}
}
lib/ui/pages/reload_page.dart
View file @
5460fac
import
'package:appframe/config/routes.dart'
;
import
'package:appframe/config/routes.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:go_router/go_router.dart'
;
///
///
/// 用于重新加载的中间路由
/// 用于重新加载的中间路由
/// 通知点击等场景先跳转到此页,再转发 targetUrl 到 /web,
/// 确保 GoRouter 完整重建 WebPage 并加载目标 URL。
///
///
class
ReloadPage
extends
StatefulWidget
{
class
ReloadPage
extends
StatefulWidget
{
const
ReloadPage
({
super
.
key
});
const
ReloadPage
({
super
.
key
});
...
@@ -23,7 +26,12 @@ class _ReloadPageState extends State<ReloadPage> {
...
@@ -23,7 +26,12 @@ class _ReloadPageState extends State<ReloadPage> {
}
}
void
_performPostDisplayOperations
()
{
void
_performPostDisplayOperations
()
{
router
.
go
(
'/web'
);
final
extra
=
GoRouterState
.
of
(
context
).
extra
;
if
(
extra
is
Map
<
String
,
dynamic
>
&&
extra
[
'targetUrl'
]
!=
null
)
{
router
.
go
(
'/web'
,
extra:
extra
);
}
else
{
router
.
go
(
'/web'
);
}
}
}
@override
@override
...
...
lib/ui/pages/web_page.dart
View file @
5460fac
...
@@ -25,6 +25,7 @@ class WebPage extends StatelessWidget {
...
@@ -25,6 +25,7 @@ class WebPage extends StatelessWidget {
var
classCode
=
extraData
?[
'classCode'
];
var
classCode
=
extraData
?[
'classCode'
];
var
userType
=
extraData
?[
'userType'
];
var
userType
=
extraData
?[
'userType'
];
var
stuId
=
extraData
?[
'stuId'
];
var
stuId
=
extraData
?[
'stuId'
];
var
targetUrl
=
extraData
?[
'targetUrl'
];
if
(
sessionCode
==
null
||
sessionCode
==
''
)
{
if
(
sessionCode
==
null
||
sessionCode
==
''
)
{
loginOpFlag
=
false
;
loginOpFlag
=
false
;
...
@@ -49,6 +50,7 @@ class WebPage extends StatelessWidget {
...
@@ -49,6 +50,7 @@ class WebPage extends StatelessWidget {
classCode:
classCode
,
classCode:
classCode
,
userType:
userType
,
userType:
userType
,
stuId:
stuId
,
stuId:
stuId
,
targetUrl:
targetUrl
,
),
),
);
);
return
webCubit
;
return
webCubit
;
...
...
lib/utils/login_util.dart
View file @
5460fac
import
'dart:convert'
;
import
'package:appframe/config/constant.dart'
;
import
'package:appframe/config/constant.dart'
;
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/config/routes.dart'
;
import
'package:appframe/config/routes.dart'
;
...
@@ -89,6 +91,8 @@ class LoginUtil {
...
@@ -89,6 +91,8 @@ class LoginUtil {
sharedPreferences
.
setString
(
'auth_className'
,
className
);
sharedPreferences
.
setString
(
'auth_className'
,
className
);
sharedPreferences
.
setString
(
'auth_stuName'
,
stuName
);
sharedPreferences
.
setString
(
'auth_stuName'
,
stuName
);
sharedPreferences
.
setString
(
'auth_relation'
,
relation
);
sharedPreferences
.
setString
(
'auth_relation'
,
relation
);
// 用于处理点击推送通知时,获取跳转到指定页面的参数
sharedPreferences
.
setString
(
'auth_roles'
,
jsonEncode
(
roles
));
if
(
loginType
==
'router'
)
{
if
(
loginType
==
'router'
)
{
router
.
go
(
router
.
go
(
...
...
pubspec.yaml
View file @
5460fac
...
@@ -95,6 +95,7 @@ dependencies:
...
@@ -95,6 +95,7 @@ dependencies:
cupertino_icons
:
^1.0.8
cupertino_icons
:
^1.0.8
fluttertoast
:
^9.0.0
fluttertoast
:
^9.0.0
flutter_local_notifications
:
^19.5.0
dev_dependencies
:
dev_dependencies
:
flutter_test
:
flutter_test
:
...
...
Write
Preview
Styling with
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment