682 lines
26 KiB
Vue
682 lines
26 KiB
Vue
<script>
|
||
import {
|
||
getGlobalSocket
|
||
} from "@/utils/socket";
|
||
import {
|
||
emitAppReturnFromBackground
|
||
} from "@/utils/backgroundRefreshBus.js";
|
||
import {
|
||
registerDeviceApi,
|
||
unregisterDeviceApi
|
||
} from "@/utils/api.js";
|
||
import {
|
||
getDeviceFingerprint
|
||
} from "@/utils/deviceFingerprint.js";
|
||
import {
|
||
initPreloadApi,
|
||
setPreloadApi,
|
||
getPreloadApi
|
||
} from '@/utils/preloadApi/index'
|
||
import {
|
||
preloadConfig
|
||
} from '@/config/preload.config'
|
||
// 静态导入(而非动态):Vite 警告"scanLaunch.js 同时被静态和动态导入"
|
||
// 既然 Header.vue 已经静态引入,这里也用静态避免冲突,模块直接进主 bundle
|
||
import {
|
||
onDeepLinkTo
|
||
} from '@/utils/scanLaunch.js'
|
||
import {
|
||
clearAllSandboxTmpFiles
|
||
} from '@/utils/ioPath.js'
|
||
|
||
// 记录上次隐藏时间的 storage key
|
||
const HIDE_TIME_KEY = "app_last_hide_time";
|
||
|
||
// 推送事件去抖:同 (type, notification_id) 在 PUSH_DEBOUNCE_MS 毫秒内只处理一次
|
||
// (避免系统重投/重复抵达)—— P1-4 修复:之前 keyed-by-type 导致
|
||
// "不同 nid 的同 type 推送"被丢弃,现在按 (type, nid) 区分。
|
||
const PUSH_DEBOUNCE_MS = 10000;
|
||
const recentByType = {};
|
||
|
||
// #ifdef APP-PLUS
|
||
/**
|
||
* 解析 plus.runtime 传入的 deep link,匹配 /verify/ 时转发给 scanLaunch.onDeepLinkTo。
|
||
* 调用方:onLaunch#handleLaunchOptions(plus.runtime.launchOptions) 冷启动,
|
||
* onLaunch#newintent listener 运行期 Android 唤起,
|
||
* onShow#handleLaunchOptions(plus.runtime.arguments) 运行期 iOS 唤起。
|
||
*
|
||
* ★ P1.1 修复:plus.runtime.arguments 在 app 生命周期内是 sticky 的,
|
||
* Universal Link 触发一次后,URL 会保留,后续每次 onShow 都会重处理,
|
||
* 把用户从当前页拽回验真页。这里用 lastConsumedDeepLink 做"已消费"标记,
|
||
* 只在 URL 变化时才重新派发,避免误跳。
|
||
*
|
||
* ★ P2 修复(2026-07-21):HBuilder Plus runtime 在 Android 自定义 scheme 唤起时,
|
||
* launchOptions 的形态不统一,常见有:
|
||
* A) string 'topfans://verify/xxx?sign=xxx'
|
||
* B) { url } { url: 'topfans://verify/xxx?sign=xxx' }
|
||
* C) { path, queryString } { path: '/verify/xxx', queryString: '?sign=xxx' }
|
||
* D) { scheme, host, path, qs} { scheme:'topfans', host:'verify', path:'/xxx', queryString:'?sign=xxx' }
|
||
* 旧实现只覆盖 A/B,C/D 形态下 sign 被丢掉,parseVerifyUrl 强校验失败静默 reject,
|
||
* 表现为"App 打开了但停在首页"。这里把四种形态都拼回完整 URL(含 query)。
|
||
*/
|
||
function handleLaunchOptions(options) {
|
||
if (!options) return
|
||
|
||
// 诊断日志:打印原始 launchOptions 形态,排查 HBuilder 各版本 / 各唤起路径差异
|
||
console.log('[handleLaunchOptions] 📥 raw:', JSON.stringify(options))
|
||
|
||
let raw = ''
|
||
if (typeof options === 'string') {
|
||
raw = options
|
||
} else if (typeof options === 'object') {
|
||
// A) { url: '完整 URL' } —— 优先取,免得自己拼错
|
||
if (options.url && typeof options.url === 'string') {
|
||
raw = options.url
|
||
}
|
||
// B) { data: '完整 URL' } —— Android newintent 部分版本 Plus Intent 形态
|
||
else if (options.data && typeof options.data === 'string') {
|
||
raw = options.data
|
||
}
|
||
// C) { path, queryString } —— iOS UniversalLinks 形态,queryString 单独成段
|
||
else if (options.path) {
|
||
const path = options.path
|
||
const qs = options.queryString || options.query || ''
|
||
// path 自身可能已经包含 ?(取决于 Plus 版本),不要再叠加
|
||
raw = `https://topfans.online${path}${path.includes('?') ? '' : qs}`
|
||
}
|
||
// D) { scheme, host, path, queryString } —— 拆分形态,自己拼回
|
||
else if (options.host) {
|
||
const scheme = options.scheme || 'topfans'
|
||
const path = options.path || '/'
|
||
const qs = options.queryString || options.query || ''
|
||
raw = `${scheme}://${options.host}${path}${qs}`
|
||
}
|
||
}
|
||
|
||
console.log('[handleLaunchOptions] 🔗 resolved:', raw)
|
||
|
||
if (!raw || !raw.includes('/verify/')) {
|
||
console.warn('[handleLaunchOptions] ⚠️ URL 不含 /verify/,跳过。raw=', raw)
|
||
return
|
||
}
|
||
// ★ P2 修复(2026-07-21):去掉 dedup,每次 deep link 唤起都直接处理。
|
||
// 旧逻辑用 lastConsumedDeepLink 去重,导致用户重复扫同一个码(同一 URL)
|
||
// 时被静默跳过,期望的"每次点都进验真页"行为丢失。
|
||
// 重复处理可能带来的副作用:同 URL 连续触发 navigateTo 会有多次 reLaunch,
|
||
// uni-app 会按顺序执行(不会冲突,因为不与 quickLogin reLaunch 并发),
|
||
// 最终稳定停在验真页上,符合用户预期。
|
||
onDeepLinkTo(raw)
|
||
}
|
||
// #endif
|
||
|
||
export default {
|
||
// 全局内存级标记,配合 uni.storage 中的 needs_welcome 一起判定
|
||
// TopfansWelcome 是否展示。onLaunch 触发时重置为 false(冷启动 = 新会话),
|
||
// 进入 square 后由 handleEnterTopfans 设为 true(本次会话内不再展示)。
|
||
globalData: {
|
||
welcomeShownThisSession: false,
|
||
// 冷启动时是否需要跳转一键登录(onLaunch 设置,onShow 消费后立即清除)
|
||
shouldShowQuickLogin: false,
|
||
},
|
||
onLaunch: function() {
|
||
console.log("App Launch");
|
||
|
||
// ★ P2 修复:清掉上一会话的 pending_scan_url,避免今天的登录被跳到昨天的验真页
|
||
// pending_scan_url 由 deep link 唤起时写入,登录后会被消费;
|
||
// 如果某次启动没消费完就退出(进程被杀 / 用户没走完登录流程),
|
||
// 下次冷启动必须清,否则会有"幽灵跳转"。
|
||
uni.removeStorageSync('pending_scan_url');
|
||
|
||
// 冷启动时检查本地 token,标记是否需要跳转到一键登录页
|
||
// 设计:只要进程被销毁(OS 杀进程 / 用户主动杀掉 / 设备重启),都视为"离开 app 再回来",
|
||
// 强制走一次 quickLogin 重新认证,与"仅按 home 键前后台切换"区分开。
|
||
// 仅 onLaunch 触发(冷启动/app 销毁后重新打开),后台切回前台不触发。
|
||
const token = uni.getStorageSync("access_token");
|
||
this.globalData.shouldShowQuickLogin = !!token;
|
||
|
||
// 【TopfansWelcome 会话判定】
|
||
// 触发场景(即用户期望展示欢迎页的入口):
|
||
// 1) "后台关闭才会在打开":用户在后台杀掉进程后重新打开 → onLaunch 触发 → 新会话
|
||
// 2) "每次第一次启动":冷启动 / 全新安装后首次启动 → onLaunch 触发 → 新会话
|
||
// 不触发的场景:
|
||
// - 仅按 home 键前后台切换(onShow 而非 onLaunch)→ 标记保持原值 → 不展示
|
||
// - 用户已经点过"进入 TOPFANS 世界" → 标记为 true → 不展示
|
||
// 注:storage 中的 needs_welcome 仍保留,由登录/注册流程负责写入,
|
||
// 只有 app 销毁(卸载/清缓存)才会随本地存储一起丢失。
|
||
this.globalData.welcomeShownThisSession = false;
|
||
// 启动早期 bootstrap 设备指纹(spec §9.1 v2.3 — X-Device-Fingerprint)
|
||
// getDeviceFingerprint 内部自包含 setStorageSync,但显式调一次可避免首次网络请求走临时生成
|
||
getDeviceFingerprint();
|
||
// 不在这里初始化 AI Chat 连接,由各页面自行管理
|
||
|
||
// ★ 新增:初始化预加载 API
|
||
try {
|
||
const api = initPreloadApi(preloadConfig)
|
||
setPreloadApi(api)
|
||
api.warmStartup()
|
||
} catch (e) {
|
||
console.warn('[preload] init failed:', e.message)
|
||
}
|
||
|
||
// ★ 启动清理:清掉旧版本产生的 doc/<业务>/tmp/ 历史残留(升级新包后第一次启动生效)
|
||
// fire-and-forget,不阻塞 onLaunch 其它初始化
|
||
// #ifdef APP-PLUS
|
||
clearAllSandboxTmpFiles().catch((e) => {
|
||
console.warn('[App] clearAllSandboxTmpFiles failed:', e?.message)
|
||
})
|
||
// #endif
|
||
|
||
// ★ 新增:清理历史下载的更新包(见 upgrade-popup.vue:_downloads/upgrade_*.wgt 累积)
|
||
this.cleanupUpgradePackages();
|
||
|
||
this.setPermissions();
|
||
|
||
// #ifdef APP-PLUS
|
||
// 冷启动 deep link:iOS UniversalLinks / Android App Links 通过 launchOptions 传入
|
||
// ★ 守卫:HBuilder 部分版本下冷启动时 launchOptions 是空对象 {},
|
||
// 此时不调用,避免日志里出现无意义的 "URL 不含 /verify/" 警告
|
||
// (onShow 的 arguments 调用会兜底处理 URL)
|
||
const _launchOptions = plus.runtime.launchOptions
|
||
if (_launchOptions && (_launchOptions.url || _launchOptions.data || _launchOptions.path || _launchOptions
|
||
.host)) {
|
||
handleLaunchOptions(_launchOptions)
|
||
}
|
||
// #endif
|
||
// #ifdef APP-PLUS
|
||
// 运行期 deep link:Android 通过 newintent 事件接收(冷启动完成后,用户从浏览器/分享唤起 App)
|
||
// ★ P2 修复:e.intent 在不同 HBuilder Plus 版本下形态不固定:
|
||
// - 部分版本是 Plus Intent 对象 { data: 'topfans://...' } → 取 .data
|
||
// - 部分版本直接是 URL string 'topfans://...' → 直接用
|
||
// - 极少数情况下是 { url: '...' } → 取 .url
|
||
// 旧实现 `e.intent?.data || {}` 在 e.intent 是 string 的版本下会变 {},
|
||
// handleLaunchOptions 收到空对象直接 return,App 停在首页。
|
||
plus.globalEvent.addEventListener('newintent', (e) => {
|
||
// ★ P2 修复:HBuilder 部分版本 newintent 事件会传 null(已知 bug),
|
||
// 旧写法 `e.intent || e || {}` 在 e=null 时直接抛 "Cannot read property 'intent' of null"。
|
||
// 用 optional chaining 包一层,e=null 时降级为 {}。
|
||
handleLaunchOptions(e?.intent || e || {})
|
||
})
|
||
// #endif
|
||
},
|
||
onShow: function() {
|
||
console.log("App Show");
|
||
|
||
// ★ P2 修复:必须先处理 deep link,让 pending_scan_url 落盘,
|
||
// 然后再走 quickLogin reLaunch 流程。
|
||
// 否则顺序倒过来时:quickLogin 登录成功后会 reLaunch 到首页,
|
||
// 而 pending_scan_url 还没存,deep link 永远丢了。
|
||
// 注意:handleLaunchOptions 在 token 已登录时会 navigateTo 验真页,
|
||
// 但后续 quickLogin reLaunch 会覆盖那一跳 —— 这是 OK 的,
|
||
// 因为我们靠的不是这一次 navigateTo,而是 pending_scan_url storage。
|
||
// quickLogin 登录后会读 storage 重新跳到验真页(见 quickLogin.vue handleQuickLogin)。
|
||
// #ifdef APP-PLUS
|
||
handleLaunchOptions(plus.runtime.arguments || {})
|
||
// #endif
|
||
|
||
// 冷启动时如果有本地 token,跳转到一键登录页(仅首次 onShow 执行一次)
|
||
// 后台切回前台时 shouldShowQuickLogin 为 false,不跳转,直接回到挂载时的页面
|
||
if (this.globalData.shouldShowQuickLogin) {
|
||
// 不在这里把 shouldShowQuickLogin 置 false —— uni.reLaunch 是异步的,
|
||
// square 作为 pages.json 第 0 项的默认 landing 会抢在 reLaunch 真正执行前 setup/mount,
|
||
// 此时如果 shouldShowQuickLogin 已经被清掉,square 就无法识别自己正处于"boot 重定向窗口期",
|
||
// 会渲染一帧 square-container(用户看到"先 square 后 welcome"的闪烁)。
|
||
// 改在 reLaunch 的 success 回调里清:此时 quickLogin 已经接管,square 已被销毁,
|
||
// 后续 quickLogin → square 的二次 reLaunch 看到的也是 false,正常流程不受影响。
|
||
this.getAllNotice();
|
||
this.clearBadgeAndNotifications();
|
||
uni.reLaunch({
|
||
url: "/pages/login/quickLogin",
|
||
success: () => {
|
||
this.globalData.shouldShowQuickLogin = false;
|
||
},
|
||
fail: () => {
|
||
// reLaunch 失败时也要清,否则后续所有页面启动都会卡在"boot 重定向"状态
|
||
this.globalData.shouldShowQuickLogin = false;
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
this.handleBackgroundReturn();
|
||
|
||
// ★ 新增:idle 预拉
|
||
try {
|
||
const api = getPreloadApi()
|
||
if (api) api.warmIdle()
|
||
} catch (e) {
|
||
// silent
|
||
}
|
||
|
||
// 从后台切回前台时重新初始化 WebSocket(onHide 中已关闭)
|
||
this.initWebSocket();
|
||
|
||
this.getAllNotice();
|
||
|
||
// 打开 App 时清除图标角标和通知栏
|
||
this.clearBadgeAndNotifications();
|
||
|
||
// ★ P2 修复:handleLaunchOptions 已经在函数开头(line 182)调过了,这里不要再调。
|
||
// 旧逻辑在上面 if(shouldShowQuickLogin) reLaunch 到 quickLogin 之后再调一次,
|
||
// 会和 quickLogin 的 reLaunch 撞车,触发 "do not operate continuously: /pages/login/quickLogin",
|
||
// 且 verify 页的 navigateTo 被丢弃。
|
||
},
|
||
onHide: function() {
|
||
console.log("App Hide");
|
||
// 关闭所有 WebSocket 连接
|
||
this.closeWebSocket();
|
||
// 记录切到后台的时间,用于 onShow 时判断是否"从后台返回"
|
||
uni.setStorageSync(HIDE_TIME_KEY, Date.now());
|
||
|
||
// 应用进入后台时创建本地通知
|
||
this.getAllNotice();
|
||
},
|
||
methods: {
|
||
initWebSocket() {
|
||
const token = uni.getStorageSync("access_token");
|
||
if (token) {
|
||
console.log("初始化全局 WebSocket 连接");
|
||
const globalSocket = getGlobalSocket();
|
||
globalSocket.init(token);
|
||
}
|
||
},
|
||
closeWebSocket() {
|
||
console.log("关闭全局 WebSocket 连接");
|
||
const globalSocket = getGlobalSocket();
|
||
globalSocket.closeAll();
|
||
},
|
||
/**
|
||
* 清理历史下载的更新包(_downloads/upgrade_*.{wgt,apk,ipa})
|
||
* - uni-upgrade-center-app 插件的 plus.downloader 默认把包放在 _downloads/upgrade_${ts}.{wgt|apk}
|
||
* - 升级弹窗关闭/下载中断/未点击安装时,这些临时包不会自动清理,会持续累积
|
||
* - 已通过 saveFile() 持久化的包在 _doc/ 目录下(路径不同),由升级弹窗 checkLocalStoragePackage 控制
|
||
*
|
||
* 各平台实际会产生哪些文件:
|
||
* - Android 整包更新 → .apk
|
||
* - Android wgt 热更新 → .wgt
|
||
* - iOS wgt 热更新 → .wgt (iOS 整包 (ipa) 走 App Store,不下载本地)
|
||
* - ipa 加入正则仅作防御:防止后端配置错误/silent update 误推到本地
|
||
*
|
||
* 启动期清理一次即可(App 正常运行期间只要不离 _downloads/ 目录就不会有新遗留)
|
||
* 仅 APP-PLUS 有效
|
||
*/
|
||
cleanupUpgradePackages() {
|
||
// #ifdef APP-PLUS
|
||
try {
|
||
const dirUrl = plus.io.convertLocalFileSystemURL("_downloads/");
|
||
plus.io.resolveLocalFileSystemURL(
|
||
dirUrl,
|
||
(dirEntry) => {
|
||
const reader = dirEntry.createReader();
|
||
reader.readEntries(
|
||
(entries) => {
|
||
entries.forEach((entry) => {
|
||
// 命名规则:见 uni-upgrade-center-app/pages/upgrade-popup.vue:_downloads/upgrade_${Date.now()}.{wgt|apk}
|
||
// ipa 在标准流程不会出现,但加入正则作防御
|
||
if (!/^upgrade_\d+\.(wgt|apk|ipa)$/i.test(entry.name)) return;
|
||
entry.remove(
|
||
() => {},
|
||
(e) =>
|
||
console.warn(
|
||
"[UPGRADE-CLEANUP] 删包失败:",
|
||
entry.fullPath,
|
||
e?.message,
|
||
),
|
||
);
|
||
});
|
||
},
|
||
(e) =>
|
||
console.warn(
|
||
"[UPGRADE-CLEANUP] 枚举 _downloads 失败:",
|
||
e?.message,
|
||
),
|
||
);
|
||
},
|
||
() => {
|
||
// _downloads 目录不存在是正常情况(从未下过更新包),静默忽略
|
||
},
|
||
);
|
||
} catch (e) {
|
||
console.warn("[UPGRADE-CLEANUP] 初始化失败:", e?.message);
|
||
}
|
||
// #endif
|
||
},
|
||
/**
|
||
* 处理"从后台切回前台"事件
|
||
* 通过 storage 中的上次隐藏时间判断本次 onShow 是否由后台返回触发
|
||
* 若是,则通知所有订阅了 useBackgroundRefresh 的页面执行刷新
|
||
*/
|
||
handleBackgroundReturn() {
|
||
const lastHide = uni.getStorageSync(HIDE_TIME_KEY) || 0;
|
||
if (!lastHide) return;
|
||
// 清理标记,避免下次普通 onShow(如首次启动)误触发
|
||
uni.removeStorageSync(HIDE_TIME_KEY);
|
||
// 时间异常保护
|
||
if (Date.now() - lastHide < 0) return;
|
||
emitAppReturnFromBackground();
|
||
},
|
||
// 获取消息列表,刚进页面时,在钩子内触发
|
||
getAllNotice() {
|
||
// 1 获取客户端推送标识信息 cid , 必须要获取到cid后才能接收推送信息
|
||
uni.getPushClientId({
|
||
success: (res) => {
|
||
// 将获取到的cid存起来,方便其它页面从缓存中获取
|
||
uni.setStorageSync("cid", res.cid);
|
||
// console.log("客户端推送标识:", res.cid);
|
||
|
||
// 1.1 把 cid + 设备信息上报给后端,后端写入 user_devices 表;
|
||
// 后续后端推送时按 user_id 查这张表拿到 cid 列表。
|
||
// 静默失败即可:即使后端没收到,App 仍能正常接收推送,只是通知中心数据对不齐。
|
||
this.reportCidToServer(res.cid);
|
||
},
|
||
fail: (err) => {
|
||
console.warn("getPushClientId failed", err);
|
||
},
|
||
});
|
||
|
||
// 2 启动监听推送消息事件
|
||
uni.onPushMessage((res) => {
|
||
const {
|
||
type,
|
||
data
|
||
} = res;
|
||
if (type == "click") {
|
||
// console.log('"click"-从系统推送服务点击消息启动应用事件;', res);
|
||
// V1.2.7 优先走 mailbox 站内信聚合页(新功能);focus+nid 命中则跳到具体消息
|
||
const focus =
|
||
res.data?.payload?.focus ||
|
||
res.data?.focus ||
|
||
data?.payload?.focus;
|
||
const nid =
|
||
res.data?.payload?.nid ||
|
||
res.data?.nid ||
|
||
data?.payload?.nid;
|
||
if (focus && nid) {
|
||
setTimeout(() => {
|
||
uni.navigateTo({
|
||
url: `/pages/mailbox/index?focus=${encodeURIComponent(focus)}&nid=${encodeURIComponent(nid)}`,
|
||
});
|
||
}, 1000);
|
||
} else if (data?.payload?.url) {
|
||
setTimeout(() => {
|
||
uni.navigateTo({
|
||
url: data.payload.url,
|
||
});
|
||
}, 1000);
|
||
} else {
|
||
console.log(data);
|
||
uni.reLaunch({
|
||
// url: "/pagesA/index/index",
|
||
});
|
||
}
|
||
}
|
||
if (type == "receive") {
|
||
// V1.2.5 修正确认:msg.payload 是 stringified JSON,内含 .title/.content/.data
|
||
console.log('"receive"-应用从推送服务器接收到推送消息事件', res);
|
||
try {
|
||
// uni.onPushMessage 的 res 形态:{ type, data: { payload: '...string', title, content, ... } }
|
||
// 不同渠道(UniPush/厂商通道)payload 字段位置可能不同,做兼容读取
|
||
const payloadStr =
|
||
res.data?.payload || res.payload || data?.payload || "";
|
||
const envelope = payloadStr ? JSON.parse(payloadStr) : {};
|
||
const payload = envelope?.data || data || {};
|
||
const t = payload.type;
|
||
const nid = payload.notification_id || "";
|
||
const debounceKey = `${t}:${nid}`;
|
||
const now = Date.now();
|
||
// 10s 同 (type, notification_id) 去抖,避免系统重投导致重复 dispatch
|
||
// (P1-4:原 key=type 会丢不同的 nid,改为组合 key)
|
||
if (debounceKey && recentByType[debounceKey] && now - recentByType[debounceKey] <
|
||
PUSH_DEBOUNCE_MS) {
|
||
console.log("[push] debounce skip", debounceKey);
|
||
return;
|
||
}
|
||
if (debounceKey) recentByType[debounceKey] = now;
|
||
// 派发到 mailbox store:更新未读计数 + (若在站内信页)PREPEND_ITEM
|
||
this.$store.dispatch("mailbox/applyPushPayload", {
|
||
data: payload,
|
||
title: envelope.title || res.data?.title || data?.title || "",
|
||
content: envelope.content || res.data?.content || data?.content || "",
|
||
});
|
||
} catch (e) {
|
||
console.error("[push] receive parse fail", e);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 上报 cid 给后端,失败不抛出(纯 best-effort)。
|
||
* 仅在已登录时才上报(JWT 在 storage);未登录时跳过,登录后再 onShow 触发一次。
|
||
*/
|
||
async reportCidToServer(cid) {
|
||
if (!cid) return;
|
||
const token = uni.getStorageSync("access_token");
|
||
if (!token) return;
|
||
try {
|
||
const sys = uni.getSystemInfoSync();
|
||
await registerDeviceApi({
|
||
cid,
|
||
// plus.os.name 在 APP-PLUS 下存在;其他平台兜底取 sys.platform
|
||
platform: (sys.osName || sys.platform || "").toLowerCase(),
|
||
appVersion: sys.appVersion || "",
|
||
deviceModel: sys.model || ""
|
||
});
|
||
// console.log("cid reported to server ok");
|
||
} catch (err) {
|
||
console.warn("cid reported to server failed:", err);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 登出时注销当前用户所有推送设备(传空 cid)。
|
||
* 调用方示例:store/modules/user.js LogoutAction 完成后 await this.unregisterAllDevices()
|
||
*/
|
||
async unregisterAllDevices() {
|
||
try {
|
||
await unregisterDeviceApi("");
|
||
} catch (err) {
|
||
console.warn("unregisterAllDevices failed:", err);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 清除 App 图标角标 + 系统通知栏中本应用的通知。
|
||
* - iOS:plus.runtime.setBadgeNumber(0) 即可清除角标。
|
||
* - Android:setBadgeNumber(0) 对部分厂商(华为/小米/OPPO/vivo/荣耀)有效;
|
||
* 不生效的机型由 UniPush SDK 推送时按未读数自行维护;
|
||
* 额外调用 NotificationManager.cancelAll() 清掉通知栏遗留的通知。
|
||
* 调用时机:onShow(用户进入 App 时)。
|
||
*/
|
||
clearBadgeAndNotifications() {
|
||
// #ifdef APP-PLUS
|
||
try {
|
||
// 1. 清除桌面图标角标
|
||
plus.runtime.setBadgeNumber(0);
|
||
} catch (e) {
|
||
console.warn("setBadgeNumber(0) failed:", e);
|
||
}
|
||
|
||
// 2. 清除系统通知栏中本应用的所有通知(Android)
|
||
if (plus.os.name === "Android") {
|
||
try {
|
||
const main = plus.android.runtimeMainActivity();
|
||
const Context = plus.android.importClass("android.content.Context");
|
||
const notificationManager = main.getSystemService(
|
||
Context.NOTIFICATION_SERVICE
|
||
);
|
||
if (notificationManager) {
|
||
plus.android.importClass("android.app.NotificationManager");
|
||
if (typeof notificationManager.cancelAll === "function") {
|
||
notificationManager.cancelAll();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 部分 Android 运行时未暴露 cancelAll,跳过即可,避免 onShow 持续报错。
|
||
}
|
||
}
|
||
// #endif
|
||
},
|
||
|
||
setPermissions() {
|
||
// #ifdef APP-PLUS
|
||
if (plus.os.name == "Android") {
|
||
// 判断是Android
|
||
var main = plus.android.runtimeMainActivity();
|
||
var pkName = main.getPackageName();
|
||
var uid = main.getApplicationInfo().plusGetAttribute("uid");
|
||
var NotificationManagerCompat = plus.android.importClass(
|
||
"android.support.v4.app.NotificationManagerCompat",
|
||
);
|
||
//android.support.v4升级为androidx
|
||
if (NotificationManagerCompat == null) {
|
||
NotificationManagerCompat = plus.android.importClass(
|
||
"androidx.core.app.NotificationManagerCompat",
|
||
);
|
||
}
|
||
var areNotificationsEnabled =
|
||
NotificationManagerCompat.from(main).areNotificationsEnabled();
|
||
// 未开通‘允许通知’权限,则弹窗提醒开通,并点击确认后,跳转到系统设置页面进行设置
|
||
if (!areNotificationsEnabled) {
|
||
uni.showModal({
|
||
title: "通知权限开启提醒",
|
||
content: "您还没有开启通知权限,无法接受到消息通知,请前往设置!",
|
||
showCancel: false,
|
||
confirmText: "去设置",
|
||
success: function(res) {
|
||
if (res.confirm) {
|
||
var Intent = plus.android.importClass("android.content.Intent");
|
||
var Build = plus.android.importClass("android.os.Build");
|
||
//android 8.0引导
|
||
if (Build.VERSION.SDK_INT >= 26) {
|
||
var intent = new Intent(
|
||
"android.settings.APP_NOTIFICATION_SETTINGS",
|
||
);
|
||
intent.putExtra("android.provider.extra.APP_PACKAGE", pkName);
|
||
} else if (Build.VERSION.SDK_INT >= 21) {
|
||
//android 5.0-7.0
|
||
var intent = new Intent(
|
||
"android.settings.APP_NOTIFICATION_SETTINGS",
|
||
);
|
||
intent.putExtra("app_package", pkName);
|
||
intent.putExtra("app_uid", uid);
|
||
} else {
|
||
//(<21)其他--跳转到该应用管理的详情页
|
||
intent.setAction(
|
||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||
);
|
||
var uri = Uri.fromParts(
|
||
"package",
|
||
mainActivity.getPackageName(),
|
||
null,
|
||
);
|
||
intent.setData(uri);
|
||
}
|
||
// 跳转到该应用的系统通知设置页
|
||
main.startActivity(intent);
|
||
}
|
||
},
|
||
});
|
||
}
|
||
} else if (plus.os.name == "iOS") {
|
||
// 判断是ISO
|
||
var isOn = undefined;
|
||
var types = 0;
|
||
var app = plus.ios.invoke("UIApplication", "sharedApplication");
|
||
var settings = plus.ios.invoke(app, "currentUserNotificationSettings");
|
||
if (settings) {
|
||
types = settings.plusGetAttribute("types");
|
||
plus.ios.deleteObject(settings);
|
||
} else {
|
||
types = plus.ios.invoke(app, "enabledRemoteNotificationTypes");
|
||
}
|
||
plus.ios.deleteObject(app);
|
||
isOn = 0 != types;
|
||
if (isOn == false) {
|
||
uni.showModal({
|
||
title: "通知权限开启提醒",
|
||
content: "您还没有开启通知权限,无法接受到消息通知,请前往设置!",
|
||
showCancel: false,
|
||
confirmText: "去设置",
|
||
success: function(res) {
|
||
if (res.confirm) {
|
||
var app = plus.ios.invoke("UIApplication", "sharedApplication");
|
||
var setting = plus.ios.invoke(
|
||
"NSURL",
|
||
"URLWithString:",
|
||
"app-settings:",
|
||
);
|
||
plus.ios.invoke(app, "openURL:", setting);
|
||
plus.ios.deleteObject(setting);
|
||
plus.ios.deleteObject(app);
|
||
}
|
||
},
|
||
});
|
||
}
|
||
}
|
||
// #endif
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<view class="app-container"> </view>
|
||
</template>
|
||
|
||
<style>
|
||
/*每个页面公共css */
|
||
|
||
/* 引入 TheMiladiatorRegular 字体 */
|
||
@font-face {
|
||
font-family: "TheMiladiatorRegular";
|
||
src: url("/static/fonts/The Miladiator Regular.ttf") format("truetype");
|
||
font-weight: normal;
|
||
font-style: normal;
|
||
font-display: swap;
|
||
}
|
||
|
||
/* 引入 ZaoZiGongFangJianHei-1 字体 */
|
||
@font-face {
|
||
font-family: "ZaoZiGongFangJianHei-1";
|
||
src: url("/static/fonts/ZaoZiGongFangJianHei-1.ttf") format("truetype");
|
||
font-weight: normal;
|
||
font-style: normal;
|
||
font-display: swap;
|
||
}
|
||
|
||
/* 引入 ZaoZiGongFangJianHei-1 字体 */
|
||
@font-face {
|
||
font-family: "JDLTYuanTiJian";
|
||
src: url("/static/fonts/JDLTYuanTiJian.ttf") format("truetype");
|
||
font-weight: normal;
|
||
font-style: normal;
|
||
font-display: swap;
|
||
}
|
||
|
||
/* 圆体 JDLTYuanTiJian.ttf 在部分 Android WebView 上报 OTS/cmap 解析失败,暂不 @font-face 加载,避免控制台告警与渲染异常 */
|
||
|
||
/* 全局字体设置 */
|
||
body {
|
||
font-family:
|
||
"JDLTYuanTiJian",
|
||
-apple-system,
|
||
BlinkMacSystemFont,
|
||
"PingFang SC",
|
||
"Hiragino Sans GB",
|
||
"Microsoft YaHei",
|
||
"Noto Sans SC",
|
||
sans-serif;
|
||
}
|
||
|
||
/* App 容器 */
|
||
.app-container {
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
position: relative;
|
||
}
|
||
|
||
.page-content {
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
}
|
||
</style> |