340 lines
12 KiB
Vue
340 lines
12 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";
|
||
|
||
// 记录上次隐藏时间的 storage key
|
||
const HIDE_TIME_KEY = "app_last_hide_time";
|
||
|
||
export default {
|
||
onLaunch: function () {
|
||
console.log("App Launch");
|
||
// 启动早期 bootstrap 设备指纹(spec §9.1 v2.3 — X-Device-Fingerprint)
|
||
// getDeviceFingerprint 内部自包含 setStorageSync,但显式调一次可避免首次网络请求走临时生成
|
||
getDeviceFingerprint();
|
||
// 不在这里初始化 AI Chat 连接,由各页面自行管理
|
||
this.setPermissions();
|
||
},
|
||
onShow: function () {
|
||
console.log("App Show");
|
||
this.handleBackgroundReturn();
|
||
|
||
this.getAllNotice();
|
||
|
||
// 打开 App 时清除图标角标和通知栏
|
||
this.clearBadgeAndNotifications();
|
||
},
|
||
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();
|
||
},
|
||
/**
|
||
* 处理"从后台切回前台"事件
|
||
* 通过 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);
|
||
if (!data?.payload?.url) {
|
||
console.log(data)
|
||
uni.reLaunch({
|
||
// url: "/pagesA/index/index",
|
||
});
|
||
} else {
|
||
setTimeout(() => {
|
||
uni.navigateTo({
|
||
url: data.payload.url,
|
||
});
|
||
}, 1000);
|
||
}
|
||
}
|
||
if (type == "receive") {
|
||
console.log('"receive"-应用从推送服务器接收到推送消息事件', res);
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 上报 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
|
||
);
|
||
notificationManager.cancelAll();
|
||
} catch (e) {
|
||
console.warn("cancelAll notifications failed:", e);
|
||
}
|
||
}
|
||
// #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>
|