<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'

	// 记录上次隐藏时间的 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)
			}

			// 2026-07-31 决议：App.vue 不再做任何启动清理。
			//   - 老版本 doc/<业务>/tmp/ 历史残留：用户进入"存储空间 → 临时文件"分类手动清
			//   - _downloads/upgrade_*.{wgt,apk,ipa}：用户进入"存储空间 → 其他业务缓存"分类手动清
			// 保留：upgrade-popup#cleanupAfterInstall（安装成功后自动清）

			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();
			},
			/**
			 * 处理"从后台切回前台"事件
			 * 通过 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;
	}

	/* 引入 JDLTYuanTiJian（经典圆体简）字体
	 *
	 * 原 OSS 上的 JDLTYuanTiJian.ttf 存在 7 处 OpenType 规范违规（表未 4 字节对齐、
	 * cmap format4 子表 length 少 2 字节、loca 缺哨兵、glyf 尾部混入填充垃圾等），
	 * 被 Android WebView 的 OTS 校验器拒绝解码，控制台报
	 * "OTS parsing error: cmap: misaligned table"，字体静默回退到系统字体。
	 *
	 * 已用 frontend/scripts/fix-broken-ttf.py 修复并转为 woff2（3.57MB -> 2.01MB），
	 * 产物通过 ots-sanitize 0 error / 0 warning 校验，字形轮廓与原字体完全一致。
	 * 改为本地加载：免去首屏网络下载，且不再依赖 OSS。
	 */
	@font-face {
		font-family: "JDLTYuanTiJian";
		src: url("/static/fonts/JDLTYuanTiJian.woff2") format("woff2");
		font-weight: normal;
		font-style: normal;
		font-display: swap;
	}

	/* 全局字体设置 */
	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>