refactor(鸿蒙元服务): 适配鸿蒙平台隐私托管与启动屏时序优化

1. 适配鸿蒙元服务平台隐私托管,由系统自动弹出隐私弹窗,应用层不再弹出自建弹窗
2. 新增启动屏strict模式,鸿蒙场景下严格等待首页DIY渲染完成再关闭启动屏,避免白屏
3. 新增splash-state单元测试覆盖新旧模式与鸿蒙场景
4. 清理隐私相关冗余代码与注释,统一平台逻辑分支
This commit is contained in:
2026-07-24 15:10:49 +08:00
parent f4a770e246
commit 45c0c2afb1
5 changed files with 503 additions and 47 deletions

View File

@@ -147,20 +147,13 @@ function getQuickAppPrivacySetting() {
}
/**
* 鸿蒙元服务: 查询平台级隐私设置
* 鸿蒙元服务的隐私授权由系统自动管理,应用层只需检查 storage
* @returns {Promise<{needAuthorization: boolean, privacyContractName?: string}>}
* 鸿蒙元服务: 已接入平台隐私托管, 应用层不再调任何系统 API
* 仅为兼容历史代码保留此函数 (被 checkPrivacy() 调用), 内部只走 storage 路径
* @returns {Promise<{needAuthorization: boolean}>}
*/
function getHarmonyPrivacySetting() {
return new Promise((resolve) => {
// #ifdef MP-HARMONY
// 鸿蒙元服务: 隐私授权由系统管理,应用层仅检查 storage
// 系统会在用户首次使用隐私API时自动弹出系统级隐私授权弹窗
resolve({ needAuthorization: !getStorageAgreed() });
// #endif
// #ifndef MP-HARMONY
resolve({ needAuthorization: !getStorageAgreed() });
// #endif
});
}
@@ -188,7 +181,7 @@ async function checkPrivacy() {
}
// #endif
// 鸿蒙元服务: 隐私由系统管理,仅查 storage
// 鸿蒙元服务: 调系统 getPrivacySetting (无 API 时 getHarmonyPrivacySetting 内部回退 storage)
// H5 / 其他平台: 仅查 storage
return checkPrivacySync();
}
@@ -730,8 +723,9 @@ const PrivacyManager = {
// 优化: storage 已同意时直接走同步快速路径, 不调 async checkPrivacy()
// 节省 1 个 microtask (~0-4ms, dev 模式更明显)
// 注意: 微信小程序/快应用/鸿蒙走 checkPrivacy (平台 API 异步)
// 注意: 微信小程序/快应用走 checkPrivacy (平台 API 异步)
// H5 下 checkPrivacy 内部已 sync 化 (修过), 直接 await 即可
// 鸿蒙元服务: 不调任何系统 API, 不弹自建弹窗 (走平台托管)
const result = _storageAgreed ? checkPrivacySync() : await checkPrivacy();
_needShow = result.needAuthorization;
@@ -741,7 +735,18 @@ const PrivacyManager = {
return;
}
// 设置待弹窗标志
// #ifdef MP-HARMONY
// 鸿蒙元服务: 已接入平台隐私托管, 由鸿蒙系统自动弹出系统级隐私弹窗
// 应用层不再 emit 'privacy:show' 触发自建 privacy-popup.vue
// 标记已决策, 防止 consumePendingShow 兜底分支在 page mounted 时再次弹自建弹窗
_needShow = false;
_pendingShow = false;
_urlRuleDecided = true;
console.log('[privacy] MP-HARMONY: 已委托鸿蒙平台隐私托管, 不触发自建弹窗');
return;
// #endif
// 其他平台: 设置待弹窗标志 + emit 触发自建弹窗
// 已 mounted 的页面组件通过事件接收
// 未 mounted 的页面组件 mounted 时通过 consumePendingShow 补偿
_pendingShow = true;
@@ -757,18 +762,28 @@ const PrivacyManager = {
* 页面 mounted 时补偿检查: 若 show() 在页面 mounted 之前 emit 了事件
* 此方法返回 true 并清除标志,页面组件据此自动弹窗
*
* 鸿蒙元服务兼容:
* 历史兼容:
* Lang.boot() 可能 hang, PrivacyManager.show() 可能从未被调用过;
* 此时 _pendingShow 仍是默认 false, 但用户从未同意过隐私, 必须强制弹一次。
* 所以增加 storage 检测: 用户未同意也返回 true (补偿弹窗)。
*
* 鸿蒙元服务(双轨制):
* 已接入平台隐私托管, 由鸿蒙系统自动弹出系统级隐私弹窗,
* 应用层不再 emit/弹自建弹窗, 兜底分支也必须返回 false, 否则会与
* 鸿蒙系统级弹窗叠加出现"多次弹出"。
* @returns {boolean} true=需要弹窗,false=不需要
*/
consumePendingShow() {
// #ifdef MP-HARMONY
// 鸿蒙元服务: 已接入平台隐私托管, 不弹自建弹窗
return false;
// #endif
if (_pendingShow && _needShow) {
_pendingShow = false;
return true;
}
// 鸿蒙兜底: PrivacyManager.show() 没跑过 + 用户没同意过 storage → 必须弹
// 兜底: PrivacyManager.show() 没跑过 + 用户没同意过 storage → 必须弹
// 同一次进程内只补一次,避免隐私弹窗被反复触发
// 关键: URL/路由规则已经决策过 "本次跳过" 时, 不应触发兜底弹窗 (白名单场景)
if (!_hasChecked && !_pendingShow && !_urlRuleDecided && !getStorageAgreed()) {

View File

@@ -136,15 +136,22 @@ class SplashState {
* @param {string} [opts.mode] - 离开动画模式
* - 'default': 默认 480ms 动画
* - 'fast': 80ms 短动画, 用于隐私弹窗等需要立即接管视觉的场景, 避免"双遮罩"感
* - 'strict': 严格模式, 不走 MOUNT_GRACE_MS / PAGE_READY_GRACE_MS 兜底,
* 只等 notifyPageReady() 或 DEFAULT_TIMEOUT 总超时
* 适用于: 已接入平台托管的系统弹窗 (鸿蒙),
* splash 必须真等到首页 diy 内容渲染好才能关
*/
hide(opts = {}) {
const reason = opts.reason || 'manual';
const useStrict = opts.mode === 'strict' || opts._strict === true;
// boot-complete 默认走软隐藏, 避免时序问题
const useSoft = opts.soft === true || (opts.soft !== false && reason === 'boot-complete');
const mode = opts.mode === 'fast' ? 'fast' : 'default';
const useSoft = useStrict || opts.soft === true || (opts.soft !== false && reason === 'boot-complete');
// strict 模式复用 fast 动画
const mode = useStrict ? 'fast' : (opts.mode === 'fast' ? 'fast' : 'default');
if (useSoft) {
this._softHide({ reason, mode });
// 传递完整 opts, _softHide 内部能识别 mode='strict' 或 _strict
this._softHide(opts);
} else {
this._doHide({ reason, mode });
}
@@ -154,10 +161,17 @@ class SplashState {
* 软隐藏: 标记应该 hide, 等组件 mount + 页面就绪
* 如果组件 1s 内没 mount, 兜底自动 hide
* 如果页面 2s 内没就绪, 兜底自动 hide
*
* strict 模式 (鸿蒙已接入平台托管):
* 不走 MOUNT_GRACE_MS / PAGE_READY_GRACE_MS 兜底
* 只等 notifyMounted() + notifyPageReady() 或 DEFAULT_TIMEOUT 总超时
* 保证 splash 一定等到首页 diy 内容真的渲染好
*/
_softHide(opts) {
if (!this._visible) return; // 已经隐藏了
this._pendingHide = opts;
// strict 模式识别: mode='strict' 或 _strict=true
const useStrict = opts && (opts.mode === 'strict' || opts._strict === true);
// 如果组件已经 mount 且页面已就绪, 立即隐藏
if (this._hasMounted && this._pageReady) {
@@ -165,6 +179,12 @@ class SplashState {
return;
}
// strict 模式: 不设 mountGraceId 兜底, 只等 notifyPageReady
// (DEFAULT_TIMEOUT 在 show() 设置的 _timeoutId 兜底)
if (useStrict) {
return;
}
// 兜底: 1s 内没 mount, 强制隐藏
if (this._mountGraceId) clearTimeout(this._mountGraceId);
this._mountGraceId = setTimeout(() => {
@@ -177,15 +197,23 @@ class SplashState {
/**
* 真正触发软隐藏
* 如果页面还未就绪, 等待页面就绪后再 hide
* strict 模式 (鸿蒙): 不设 pageReadyGraceId 兜底, 只等 notifyPageReady()
*/
_flushPendingHide() {
if (this._mountGraceId) {
clearTimeout(this._mountGraceId);
this._mountGraceId = null;
}
const _opts = this._pendingHide || {}
const useStrict = _opts && (_opts.mode === 'strict' || _opts._strict === true)
// 如果页面还未就绪, 等待页面就绪 (保留 _pendingHide, 等页面就绪后再处理)
if (!this._pageReady) {
// strict 模式: 不设 pageReadyGraceId, 只等 notifyPageReady()
// 总超时由 show() 里的 _timeoutId 兜底 (DEFAULT_TIMEOUT=8s)
if (useStrict) {
return;
}
// 兜底: 2s 内没就绪, 强制隐藏
if (this._pageReadyGraceId) clearTimeout(this._pageReadyGraceId);
this._pageReadyGraceId = setTimeout(() => {