|
@@ -2,6 +2,9 @@ import { Injectable, inject } from '@angular/core';
|
|
|
import { isNativeApp } from './api/platform-proxy.service';
|
|
import { isNativeApp } from './api/platform-proxy.service';
|
|
|
import { DeepLinkService } from './deep-link.service';
|
|
import { DeepLinkService } from './deep-link.service';
|
|
|
|
|
|
|
|
|
|
+/** 通知渠道 id(Android 8+ 必须先创建同名渠道,否则通知被静默丢弃) */
|
|
|
|
|
+const NOTIFY_CHANNEL_ID = 'fmode-default';
|
|
|
|
|
+
|
|
|
/** 一条要弹出的通知 */
|
|
/** 一条要弹出的通知 */
|
|
|
export interface AppNotice {
|
|
export interface AppNotice {
|
|
|
/** 标题(通知栏第一行) */
|
|
/** 标题(通知栏第一行) */
|
|
@@ -173,6 +176,7 @@ export class AppNotificationService {
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const { LocalNotifications } = await import('@capacitor/local-notifications');
|
|
const { LocalNotifications } = await import('@capacitor/local-notifications');
|
|
|
|
|
+ await this.ensureChannel(LocalNotifications);
|
|
|
await LocalNotifications.schedule({
|
|
await LocalNotifications.schedule({
|
|
|
notifications: [
|
|
notifications: [
|
|
|
{
|
|
{
|
|
@@ -184,7 +188,7 @@ export class AppNotificationService {
|
|
|
/* 点击后自动清除,不堆积 */
|
|
/* 点击后自动清除,不堆积 */
|
|
|
autoCancel: true,
|
|
autoCancel: true,
|
|
|
smallIcon: 'ic_stat_icon_config_sample',
|
|
smallIcon: 'ic_stat_icon_config_sample',
|
|
|
- channelId: 'fmode-default',
|
|
|
|
|
|
|
+ channelId: NOTIFY_CHANNEL_ID,
|
|
|
/*
|
|
/*
|
|
|
* 🔴 必须显式关掉「精确通知」(2026-11-15 真机踩坑)。
|
|
* 🔴 必须显式关掉「精确通知」(2026-11-15 真机踩坑)。
|
|
|
*
|
|
*
|
|
@@ -210,9 +214,45 @@ export class AppNotificationService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建通知渠道(Android 8+ 必需,2026-11-15 真机踩坑)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 🔴 症状:`schedule()` 返回成功、无任何报错,但**通知栏里什么都没有**。
|
|
|
|
|
+ * 原因是我们指定了 `channelId: fmode-default`,而这个渠道**从未被创建**。
|
|
|
|
|
+ * Android 8.0+ 规定:目标渠道不存在时,通知被系统**静默丢弃**——
|
|
|
|
|
+ * 插件不做校验,JS 侧也就完全看不到失败。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 这是最容易被误判成「权限没开」的一类问题,实际权限是完全正常的。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 幂等:重复调用只会更新已有渠道的属性,可以每次发通知前都调一次,
|
|
|
|
|
+ * 也可以只在首次调 —— 这里用 `channelReady` 缓存,省一次跨桥调用。
|
|
|
|
|
+ */
|
|
|
|
|
+ private channelReady = false;
|
|
|
|
|
+
|
|
|
|
|
+ private async ensureChannel(
|
|
|
|
|
+ LN: typeof import('@capacitor/local-notifications').LocalNotifications,
|
|
|
|
|
+ ): Promise<void> {
|
|
|
|
|
+ if (this.channelReady) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await LN.createChannel({
|
|
|
|
|
+ id: NOTIFY_CHANNEL_ID,
|
|
|
|
|
+ name: '消息与任务',
|
|
|
|
|
+ description: '容器任务完成与新消息提醒',
|
|
|
|
|
+ importance: 4, // HIGH:有声音且弹出横幅,否则只有抽屉里一条
|
|
|
|
|
+ visibility: 1, // PUBLIC:锁屏也显示内容
|
|
|
|
|
+ vibration: true,
|
|
|
|
|
+ });
|
|
|
|
|
+ this.channelReady = true;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 创建失败不阻断发送:可能是低版本 Android(无渠道概念),
|
|
|
|
|
+ * 那种情况下不传 channelId 也能正常弹。
|
|
|
|
|
+ */
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/** 同一 key 复用同一个 id → 新通知覆盖旧通知 */
|
|
/** 同一 key 复用同一个 id → 新通知覆盖旧通知 */
|
|
|
- private idForKey(key: string): number {
|
|
|
|
|
- const hit = this.idsByKey.get(key);
|
|
|
|
|
|
|
+ private idForKey(key: string): number { const hit = this.idsByKey.get(key);
|
|
|
if (hit !== undefined) return hit;
|
|
if (hit !== undefined) return hit;
|
|
|
const id = this.freshId();
|
|
const id = this.freshId();
|
|
|
this.idsByKey.set(key, id);
|
|
this.idsByKey.set(key, id);
|