feat: 新增站内消息系统,支持通知与订单跳转
This commit is contained in:
1 parent
411ec8be23
commit
c799b62f6b
14 files changed
+464
-9
No files matched your search
@@ -13,6 +13,8 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
// flutter_local_notifications 需要 core library desugaring
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@@ -42,3 +44,7 @@ android {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<!-- 访问局域网后端接口(release 包也需要) -->
|
<!-- 访问局域网后端接口(release 包也需要) -->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<!-- Android 13+ 展示系统通知 -->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
<!-- 后台保持进程存活,持续轮询派单消息 -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
<application
|
<application
|
||||||
android:label="smart_grid_iot_app_flutter"
|
android:label="smart_grid_iot_app_flutter"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
@@ -28,6 +33,11 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- 后台保活:让 App 退到后台后仍能轮询站内消息 -->
|
||||||
|
<service
|
||||||
|
android:name=".KeepAliveService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync"/>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package com.voltlink.smart_grid_iot_app_flutter
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前台服务:仅用于让进程在退到后台后保持存活,
|
||||||
|
* 从而让 Flutter 侧的 30s 轮询继续运行并弹出系统通知。
|
||||||
|
*
|
||||||
|
* 它不是推送通道:用户手动「强行停止」或清理后台后仍会中断。
|
||||||
|
*/
|
||||||
|
class KeepAliveService : Service() {
|
||||||
|
companion object {
|
||||||
|
private const val CHANNEL_ID = "voltlink_keepalive"
|
||||||
|
private const val NOTIFICATION_ID = 1001
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
createChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
startForeground(NOTIFICATION_ID, buildNotification())
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"后台接单提醒",
|
||||||
|
NotificationManager.IMPORTANCE_MIN
|
||||||
|
)
|
||||||
|
channel.description = "保持 App 运行以接收派单消息"
|
||||||
|
channel.setShowBadge(false)
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(): Notification {
|
||||||
|
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
Notification.Builder(this, CHANNEL_ID)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Notification.Builder(this)
|
||||||
|
}
|
||||||
|
return builder
|
||||||
|
.setContentTitle("VoltLink 正在后台运行")
|
||||||
|
.setContentText("用于及时接收派单消息")
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-1
@@ -1,5 +1,37 @@
|
|||||||
package com.voltlink.smart_grid_iot_app_flutter
|
package com.voltlink.smart_grid_iot_app_flutter
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class MainActivity : FlutterActivity()
|
class MainActivity : FlutterActivity() {
|
||||||
|
private val channelName = "voltlink/keepalive"
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
|
||||||
|
.setMethodCallHandler { call, result ->
|
||||||
|
when (call.method) {
|
||||||
|
"start" -> {
|
||||||
|
startKeepAlive()
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
"stop" -> {
|
||||||
|
stopService(Intent(this, KeepAliveService::class.java))
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startKeepAlive() {
|
||||||
|
val intent = Intent(this, KeepAliveService::class.java)
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||||
|
startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
-2
@@ -1,19 +1,67 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../core/app_keys.dart';
|
import '../core/app_keys.dart';
|
||||||
|
import '../core/notifications/keep_alive.dart';
|
||||||
|
import '../core/notifications/local_notifications.dart';
|
||||||
import '../core/theme/app_theme.dart';
|
import '../core/theme/app_theme.dart';
|
||||||
import '../core/theme/app_typography.dart';
|
import '../core/theme/app_typography.dart';
|
||||||
|
import '../features/auth/application/session_controller.dart';
|
||||||
|
import '../features/notice/application/notice_alert_controller.dart';
|
||||||
import 'router.dart';
|
import 'router.dart';
|
||||||
|
|
||||||
class VoltLinkApp extends ConsumerWidget {
|
class VoltLinkApp extends ConsumerStatefulWidget {
|
||||||
const VoltLinkApp({super.key});
|
const VoltLinkApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<VoltLinkApp> createState() => _VoltLinkAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VoltLinkAppState extends ConsumerState<VoltLinkApp> {
|
||||||
|
StreamSubscription<String>? _tapSub;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// 运行期点击系统通知 → 跳转对应页面
|
||||||
|
_tapSub = LocalNotifications.instance.taps.listen(_openRoute);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// 冷启动由通知拉起时,初始化阶段已把路由暂存下来
|
||||||
|
final route = LocalNotifications.instance.takeLaunchRoute();
|
||||||
|
if (route != null) _openRoute(route);
|
||||||
|
// 已登录则开启 Android 前台服务
|
||||||
|
unawaited(
|
||||||
|
BackgroundKeepAlive.setEnabled(ref.read(sessionProvider).isLoggedIn),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tapSub?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openRoute(String route) {
|
||||||
|
if (!mounted || route.isEmpty) return;
|
||||||
|
ref.read(routerProvider).go(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final router = ref.watch(routerProvider);
|
final router = ref.watch(routerProvider);
|
||||||
|
|
||||||
|
// 登录/登出时同步 Android 前台服务开关
|
||||||
|
ref.listen(sessionProvider.select((s) => s.isLoggedIn), (_, loggedIn) {
|
||||||
|
unawaited(BackgroundKeepAlive.setEnabled(loggedIn));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 激活站内消息提醒轮询
|
||||||
|
ref.watch(noticeAlertProvider);
|
||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: 'VoltLink',
|
title: 'VoltLink',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// Android 前台服务开关:让 App 在退到后台后仍保持进程存活,
|
||||||
|
/// 从而继续轮询站内消息并弹系统通知。
|
||||||
|
///
|
||||||
|
/// 注意:这不是推送。用户手动「强行停止」/清理后台后服务会终止,
|
||||||
|
/// 需要被杀进程后仍送达时必须接入厂商推送或极光/个推。
|
||||||
|
class BackgroundKeepAlive {
|
||||||
|
BackgroundKeepAlive._();
|
||||||
|
|
||||||
|
static const _channel = MethodChannel('voltlink/keepalive');
|
||||||
|
|
||||||
|
static Future<void> setEnabled(bool enabled) async {
|
||||||
|
if (!Platform.isAndroid) return;
|
||||||
|
try {
|
||||||
|
await _channel.invokeMethod(enabled ? 'start' : 'stop');
|
||||||
|
} on PlatformException {
|
||||||
|
// 无通知权限或系统限制时静默失败,不影响主流程。
|
||||||
|
} on MissingPluginException {
|
||||||
|
// 非 Android 宿主或插件未注册。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
|
||||||
|
/// 本地系统通知。Android 专用(当前电工端只在 Android 分发)。
|
||||||
|
///
|
||||||
|
/// 只负责「展示通知」与「回传点击 payload」,消息从哪来由调用方决定。
|
||||||
|
class LocalNotifications {
|
||||||
|
LocalNotifications._();
|
||||||
|
|
||||||
|
static final instance = LocalNotifications._();
|
||||||
|
|
||||||
|
static const channelId = 'voltlink_dispatch';
|
||||||
|
static const channelName = '派单与站内消息';
|
||||||
|
static const channelDesc = '收到新的派单或站内消息时提醒';
|
||||||
|
|
||||||
|
final _plugin = FlutterLocalNotificationsPlugin();
|
||||||
|
final _taps = StreamController<String>.broadcast();
|
||||||
|
|
||||||
|
/// 冷启动时由通知拉起的路由(初始化完成时读取一次)。
|
||||||
|
String? _launchRoute;
|
||||||
|
|
||||||
|
bool _ready = false;
|
||||||
|
|
||||||
|
/// 运行期点击通知的路由流。
|
||||||
|
Stream<String> get taps => _taps.stream;
|
||||||
|
|
||||||
|
/// 在 `runApp` 之前调用。
|
||||||
|
Future<void> init() async {
|
||||||
|
if (_ready || !Platform.isAndroid) return;
|
||||||
|
const settings = InitializationSettings(
|
||||||
|
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
||||||
|
);
|
||||||
|
await _plugin.initialize(
|
||||||
|
settings: settings,
|
||||||
|
onDidReceiveNotificationResponse: _onTap,
|
||||||
|
);
|
||||||
|
|
||||||
|
final android = _plugin
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin
|
||||||
|
>();
|
||||||
|
// Android 13+ 需要用户授权,否则通知不会展示
|
||||||
|
await android?.requestNotificationsPermission();
|
||||||
|
await android?.createNotificationChannel(
|
||||||
|
const AndroidNotificationChannel(
|
||||||
|
channelId,
|
||||||
|
channelName,
|
||||||
|
description: channelDesc,
|
||||||
|
importance: Importance.high,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final launch = await _plugin.getNotificationAppLaunchDetails();
|
||||||
|
if ((launch?.didNotificationLaunchApp ?? false)) {
|
||||||
|
final payload = launch?.notificationResponse?.payload;
|
||||||
|
if (payload != null && payload.isNotEmpty) _launchRoute = payload;
|
||||||
|
}
|
||||||
|
_ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 取出冷启动通知携带的路由(只返回一次)。
|
||||||
|
String? takeLaunchRoute() {
|
||||||
|
final route = _launchRoute;
|
||||||
|
_launchRoute = null;
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> show({
|
||||||
|
required int id,
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
required String route,
|
||||||
|
}) async {
|
||||||
|
if (!_ready) return;
|
||||||
|
await _plugin.show(
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
body: body,
|
||||||
|
notificationDetails: const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
channelId,
|
||||||
|
channelName,
|
||||||
|
channelDescription: channelDesc,
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
payload: route,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTap(NotificationResponse response) {
|
||||||
|
final payload = response.payload;
|
||||||
|
if (payload != null && payload.isNotEmpty) _taps.add(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ class TokenStorage {
|
|||||||
static const _kUserInfo = 'userInfo';
|
static const _kUserInfo = 'userInfo';
|
||||||
static const _kServerHost = 'serverHost';
|
static const _kServerHost = 'serverHost';
|
||||||
static const _kServerPort = 'serverPort';
|
static const _kServerPort = 'serverPort';
|
||||||
|
static const _kNoticeAlertMaxId = 'noticeAlertMaxId';
|
||||||
|
|
||||||
static Future<TokenStorage> open() async =>
|
static Future<TokenStorage> open() async =>
|
||||||
TokenStorage(await SharedPreferences.getInstance());
|
TokenStorage(await SharedPreferences.getInstance());
|
||||||
@@ -55,5 +56,11 @@ class TokenStorage {
|
|||||||
await _prefs.remove(_kUserInfo);
|
await _prefs.remove(_kUserInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 已弹过系统通知的最大消息 id,避免重启后重复提醒历史消息。
|
||||||
|
int get noticeAlertMaxId => _prefs.getInt(_kNoticeAlertMaxId) ?? -1;
|
||||||
|
|
||||||
|
Future<void> setNoticeAlertMaxId(int id) =>
|
||||||
|
_prefs.setInt(_kNoticeAlertMaxId, id);
|
||||||
|
|
||||||
bool get hasSession => accessToken.isNotEmpty;
|
bool get hasSession => accessToken.isNotEmpty;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../../core/network/api_response.dart';
|
||||||
|
import '../../../core/notifications/local_notifications.dart';
|
||||||
|
import '../../../core/providers.dart';
|
||||||
|
import '../../auth/application/session_controller.dart';
|
||||||
|
import 'notice_routes.dart';
|
||||||
|
|
||||||
|
/// 轮询未读站内消息,并在 App 不处于前台时弹出系统通知。
|
||||||
|
///
|
||||||
|
/// 登录后每 30s 拉一次未读列表;已提醒过的最大消息 id 持久化,
|
||||||
|
/// 重启后不会重复提醒历史消息。
|
||||||
|
final noticeAlertProvider =
|
||||||
|
NotifierProvider<NoticeAlertController, int>(NoticeAlertController.new);
|
||||||
|
|
||||||
|
class NoticeAlertController extends Notifier<int> {
|
||||||
|
static const _interval = Duration(seconds: 30);
|
||||||
|
|
||||||
|
Timer? _timer;
|
||||||
|
bool _busy = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int build() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
|
||||||
|
final loggedIn = ref.watch(sessionProvider.select((s) => s.isLoggedIn));
|
||||||
|
if (!loggedIn) return 0;
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
});
|
||||||
|
_timer = Timer.periodic(_interval, (_) => unawaited(_poll()));
|
||||||
|
unawaited(_poll());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _poll() async {
|
||||||
|
if (_busy) return;
|
||||||
|
_busy = true;
|
||||||
|
try {
|
||||||
|
final res = await ref
|
||||||
|
.read(noticeApiProvider)
|
||||||
|
.list(pageSize: 20, isRead: false);
|
||||||
|
if (!res.isOk || res.data == null || res.data!.list.isEmpty) return;
|
||||||
|
|
||||||
|
final notices = res.data!.list;
|
||||||
|
final latest = notices.fold<int>(0, (m, n) => n.id > m ? n.id : m);
|
||||||
|
final storage = ref.read(tokenStorageProvider);
|
||||||
|
|
||||||
|
// 首次运行只记录基线,避免把历史未读一次性弹成通知
|
||||||
|
final maxId = storage.noticeAlertMaxId;
|
||||||
|
if (maxId < 0) {
|
||||||
|
await storage.setNoticeAlertMaxId(latest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final fresh = notices.where((n) => n.id > maxId).toList()
|
||||||
|
..sort((a, b) => a.id.compareTo(b.id));
|
||||||
|
if (fresh.isEmpty) return;
|
||||||
|
|
||||||
|
// 前台时只更新基线(界面内已有红点/消息中心),退到后台才弹通知
|
||||||
|
final inBackground =
|
||||||
|
WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed;
|
||||||
|
if (inBackground) {
|
||||||
|
for (final notice in fresh) {
|
||||||
|
await LocalNotifications.instance.show(
|
||||||
|
id: notice.id,
|
||||||
|
title: notice.title,
|
||||||
|
body: notice.content,
|
||||||
|
route: noticeRoute(notice) ?? '/notice',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await storage.setNoticeAlertMaxId(latest);
|
||||||
|
} on ApiException {
|
||||||
|
// 网络波动忽略,下一轮再试
|
||||||
|
} finally {
|
||||||
|
_busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import '../data/notice_api.dart';
|
||||||
|
|
||||||
|
/// 消息可跳转的订单路由;无法识别业务类型时返回 null。
|
||||||
|
///
|
||||||
|
/// 与后端通知投递的 `bizType` 约定一致:
|
||||||
|
/// `survey_order` / `repair_dispatch` / `install_order` / `maintenance_order`。
|
||||||
|
String? noticeRoute(Notice notice) {
|
||||||
|
final id = notice.bizId;
|
||||||
|
if (id == null || id <= 0) return null;
|
||||||
|
final source = switch (notice.bizType) {
|
||||||
|
'survey_order' => 'survey',
|
||||||
|
'repair_dispatch' || 'repair_order' => 'repair',
|
||||||
|
'install_order' => 'install',
|
||||||
|
'maintenance_order' => 'maintenance',
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
return source == null ? null : '/order/$source/$id';
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import '../../../shared/widgets/app_feedback.dart';
|
|||||||
import '../../../shared/widgets/x_card.dart';
|
import '../../../shared/widgets/x_card.dart';
|
||||||
import '../../../shared/widgets/x_common.dart';
|
import '../../../shared/widgets/x_common.dart';
|
||||||
import '../../../shared/widgets/x_top_bar.dart';
|
import '../../../shared/widgets/x_top_bar.dart';
|
||||||
|
import '../application/notice_routes.dart';
|
||||||
import '../data/notice_api.dart';
|
import '../data/notice_api.dart';
|
||||||
|
|
||||||
/// 消息中心:站内通知列表 + 已读标记 + 分页加载。
|
/// 消息中心:站内通知列表 + 已读标记 + 分页加载。
|
||||||
@@ -108,14 +109,11 @@ class _NoticeCenterPageState extends ConsumerState<NoticeCenterPage> {
|
|||||||
|
|
||||||
Future<void> _loadMore() => _load();
|
Future<void> _loadMore() => _load();
|
||||||
|
|
||||||
/// 点击消息:先标记已读;勘察指派消息(`bizType == "survey_order"`)
|
/// 点击消息:先标记已读;能识别业务来源时跳转到对应订单详情。
|
||||||
/// 带 `bizId` 时跳转到对应勘察订单详情,其余保持仅标记已读。
|
|
||||||
Future<void> _onNoticeTap(Notice notice) async {
|
Future<void> _onNoticeTap(Notice notice) async {
|
||||||
if (!notice.isRead) await _markRead(notice);
|
if (!notice.isRead) await _markRead(notice);
|
||||||
final bizId = notice.bizId;
|
final route = noticeRoute(notice);
|
||||||
if (notice.bizType == 'survey_order' && bizId != null && mounted) {
|
if (route != null && mounted) context.push(route);
|
||||||
context.push('/order/survey/$bizId');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _markRead(Notice notice) async {
|
Future<void> _markRead(Notice notice) async {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'app/app.dart';
|
import 'app/app.dart';
|
||||||
|
import 'core/notifications/local_notifications.dart';
|
||||||
import 'core/providers.dart';
|
import 'core/providers.dart';
|
||||||
import 'core/storage/token_storage.dart';
|
import 'core/storage/token_storage.dart';
|
||||||
|
|
||||||
@@ -11,6 +12,9 @@ Future<void> main() async {
|
|||||||
// SharedPreferences 必须在 runApp 之前完成异步读取,再通过 override 注入。
|
// SharedPreferences 必须在 runApp 之前完成异步读取,再通过 override 注入。
|
||||||
final storage = await TokenStorage.open();
|
final storage = await TokenStorage.open();
|
||||||
|
|
||||||
|
// 系统通知渠道 + Android 13 通知权限;冷启动点击通知的路由也会在此暂存。
|
||||||
|
await LocalNotifications.instance.init();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [tokenStorageProvider.overrideWithValue(storage)],
|
overrides: [tokenStorageProvider.overrideWithValue(storage)],
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.9"
|
version: "1.0.9"
|
||||||
|
dbus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dbus
|
||||||
|
sha256: f7db8504e715b835f5ebe5fe74604ccd961d504c73389a5d8e81bafc2e2f3612
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.0"
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -206,6 +214,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_local_notifications:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications
|
||||||
|
sha256: "9ee28742343dd13df758bb4185e8ded37ac2d1aeca6648c3ba7862c99797517e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "22.3.2"
|
||||||
|
flutter_local_notifications_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_linux
|
||||||
|
sha256: "3b89a4bf095fa5f8a99beeabf982c5bd506c3ad25e7b9e61da945e994dc5b615"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.2"
|
||||||
|
flutter_local_notifications_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_platform_interface
|
||||||
|
sha256: "43c3761d916c9bd3d5c7ebbc44d82f4990329840c0c5d62ad5260cc1b5d399bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.2.0"
|
||||||
|
flutter_local_notifications_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_web
|
||||||
|
sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
flutter_local_notifications_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_windows
|
||||||
|
sha256: "6f43bdd03b171b7a90f22647506fea33e2bb12294b7c7c7a3d690e960a382945"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -730,6 +778,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.16"
|
version: "0.6.16"
|
||||||
|
timezone:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: timezone
|
||||||
|
sha256: "9f4e5a1a0e4fef7dd3424842c0575db01bfb7a11730e8d55753fcd5c5c98767b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.2"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ dependencies:
|
|||||||
flutter_svg: ^2.3.0
|
flutter_svg: ^2.3.0
|
||||||
image_picker: ^1.2.3
|
image_picker: ^1.2.3
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
|
flutter_local_notifications: ^22.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in new issue
Block a user