topfans/frontend/App.vue

682 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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
}
// 从后台切回前台时重新初始化 WebSocketonHide 中已关闭)
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>