feat: 实现隐私弹窗全链路功能,支持本地配置与远程patch融合
- 新增隐私管理器核心逻辑,支持URL路由规则匹配、远程patch签名校验与融合 - 实现启动期资源等待机制,确保弹窗在i18n、主题配置就绪后触发 - 新增启动进度条组件,展示启动阶段进度与状态 - 重构隐私弹窗组件生命周期,修复事件丢失问题并支持规则动态重评估 - 适配多端平台隐私检查逻辑,支持微信小程序、鸿蒙元服务与H5 - 支持uniacid租户隔离的持久化规则存储
This commit is contained in:
@@ -94,11 +94,130 @@ export const apiConfig = {
|
||||
retryDelay: 1000
|
||||
};
|
||||
|
||||
// 隐私弹窗配置 (与 langConfig/themeConfig 同构)
|
||||
// 本地兜底 + 远程 patch (来自 /api/config/init data.privacy_url_rules) 融合
|
||||
// source: 'local' - 仅本地兜底, 忽略远程 patch
|
||||
// 'remote' - 本地兜底 + 远程 patch 覆盖 (默认)
|
||||
// 'hybrid' - 同 remote, 远程失败时退回本地
|
||||
// 'remote-only' - 完全依赖远程 patch, 本地兜底为空
|
||||
export const privacyConfig = {
|
||||
// 本地兜底规则 (mode: 'all'|'only'|'skip', patterns: string[])
|
||||
// 远程 patch 由 store/index.js 的 init action 通过 PrivacyManager.applyWithMerge() 融合
|
||||
// patterns 写法:
|
||||
// - 子串匹配 + 结尾匹配 (双通道), 业务侧可以写完整路径或相对路径
|
||||
// - subPackage 路由 (如 /pages_tool/member/index) 用短路径 'member/index' 即可命中
|
||||
urlRulesBase: {
|
||||
// 场景 2: 只有首页、商品详情、个人中心才弹, 其他页面都不弹
|
||||
mode: 'only',
|
||||
patterns: [
|
||||
// 首页 (含 subPackage 重定向后也是 pages/index/index)
|
||||
'pages/index/index',
|
||||
// 商品详情 (subPackage 内可能为 pages_goods/goods/detail/detail, 这里用末尾匹配)
|
||||
'goods/detail/detail',
|
||||
// 个人中心 (subPackage 内通常为 pages_tool/member/index)
|
||||
'member/index',
|
||||
],
|
||||
},
|
||||
// dataSource: 与 langConfig.source 同构
|
||||
// 'local' 纯本地, 忽略 /api/config/init 的 privacy_url_rules
|
||||
// 'remote' 本地 + 远程 patch (远程失败时仍用本地)
|
||||
// 'hybrid' 同 remote, 语义别名
|
||||
// 'remote-only' 必须有远程 patch, 本地空 patterns → 一律不弹
|
||||
source: 'remote',
|
||||
// 启动期等待通道 (弹窗必须等这些 promise 才弹)
|
||||
// waitLang: Lang.boot() - i18n 数据就绪
|
||||
// waitTheme: applyThemeWithRemote() - 本地 preset 应用
|
||||
// waitThemeOverride: ThemeOverride.boot() - brand/light/dark 三桶 token
|
||||
// waitRemoteConfig: /api/config/init (含 privacy_url_rules 远程 patch)
|
||||
waitChannels: {
|
||||
lang: true,
|
||||
theme: true,
|
||||
themeOverride: true,
|
||||
remoteConfig: true,
|
||||
},
|
||||
// 总超时熔断 (ms): 所有等待通道超过这个时间强制放行, 不阻塞弹窗
|
||||
// 取 langConfig.remote.bootTimeout (5000) 和 themeConfig.overrideRemote.bootTimeout (5000) 的最大值
|
||||
// + 1000ms 余量 → 6000ms. 与原硬编码值一致, 但可通过修改此字段调整.
|
||||
// 业务侧想"宁愿晚一点也要全资源就绪" → 调大; 想"快速失败" → 调小.
|
||||
bootTimeout: 6000,
|
||||
// 远程 patch 签名密钥 (与 langConfig.remote.signSecret 同构)
|
||||
// 后端约定: hash_hmac('sha256', JSON.stringify(patch), secret)
|
||||
// secret='' → 跳过校验, 接受任意 patch (向后兼容老后端)
|
||||
// secret='xxx' → 校验 patch.sign, 不匹配则拒绝
|
||||
// 留空默认不校验. 前端实现是 btoa 弱校验, 仅防简单篡改, 强校验建议用 crypto-js.
|
||||
signSecret: '',
|
||||
// 单次 /api/config/init 请求超时 (ms)
|
||||
// 复用 apiConfig.timeout, 不单独配置
|
||||
// remote: {
|
||||
// // 远程 patch 字段名 (后端 /api/config/init 返回 data.privacy_url_rules)
|
||||
// fieldPath: 'privacy_url_rules',
|
||||
// // 切租户/uniacid 时强制重拉 (远端 patch 重新下发)
|
||||
// refreshOnUniacidChange: true,
|
||||
// // 本地缓存 key 前缀: <prefix><uniacid>
|
||||
// storagePrefix: 'privacy_url_rules_',
|
||||
// },
|
||||
};
|
||||
|
||||
// 配置外置管理类
|
||||
class ConfigExternal {
|
||||
constructor() {
|
||||
this.loadedConfigs = {};
|
||||
this.loadPromises = {};
|
||||
// 启动期各通道的 await promise (由 App.vue onLaunch 注入)
|
||||
// 隐私弹窗 show() 时通过 PrivacyManager.getReadyWaiters() 拿到 → 串成 readyGuard
|
||||
this.readyWaiters = {
|
||||
lang: null,
|
||||
theme: null,
|
||||
themeOverride: null,
|
||||
remoteConfig: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置启动期各通道的 await promise
|
||||
* App.vue onLaunch 在 step 2 注入, PrivacyManager.show() 时读取
|
||||
* @param {Object} waiters { lang?, theme?, themeOverride?, remoteConfig? }
|
||||
*/
|
||||
setReadyWaiters(waiters) {
|
||||
if (!waiters || typeof waiters !== 'object') return
|
||||
for (const k of Object.keys(this.readyWaiters)) {
|
||||
if (waiters[k] && typeof waiters[k].then === 'function') {
|
||||
this.readyWaiters[k] = waiters[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取隐私弹窗 readyGuard (合并所有启用的等待通道)
|
||||
* 任意通道 disabled → 不等
|
||||
* 所有通道都 null → 返回 Promise.resolve() (不退化)
|
||||
* @param {Object} channels { lang?, theme?, themeOverride?, remoteConfig? }
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getPrivacyReadyGuard(channels) {
|
||||
const cfg = (typeof channels === 'object' && channels) || (privacyConfig.waitChannels || {})
|
||||
const promises = []
|
||||
for (const k of ['lang', 'theme', 'themeOverride', 'remoteConfig']) {
|
||||
if (cfg[k] && this.readyWaiters[k]) {
|
||||
promises.push(this.readyWaiters[k])
|
||||
}
|
||||
}
|
||||
if (!promises.length) return Promise.resolve()
|
||||
return Promise.all(promises).catch(() => {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐私弹窗总超时熔断 (ms)
|
||||
* 默认取 langConfig.remote.bootTimeout 与 themeConfig.overrideRemote.bootTimeout 的最大值 + 1000ms 余量
|
||||
* 业务侧可通过 privacyConfig.bootTimeout 覆盖
|
||||
*/
|
||||
getPrivacyBootTimeout() {
|
||||
if (privacyConfig && typeof privacyConfig.bootTimeout === 'number' && privacyConfig.bootTimeout > 0) {
|
||||
return privacyConfig.bootTimeout
|
||||
}
|
||||
const langT = (langConfig && langConfig.remote && langConfig.remote.bootTimeout) || 5000
|
||||
const themeT = (themeConfig && themeConfig.overrideRemote && themeConfig.overrideRemote.bootTimeout) || 5000
|
||||
return Math.max(langT, themeT) + 1000
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user