feat: 实现隐私弹窗全链路功能,支持本地配置与远程patch融合

- 新增隐私管理器核心逻辑,支持URL路由规则匹配、远程patch签名校验与融合
- 实现启动期资源等待机制,确保弹窗在i18n、主题配置就绪后触发
- 新增启动进度条组件,展示启动阶段进度与状态
- 重构隐私弹窗组件生命周期,修复事件丢失问题并支持规则动态重评估
- 适配多端平台隐私检查逻辑,支持微信小程序、鸿蒙元服务与H5
- 支持uniacid租户隔离的持久化规则存储
This commit is contained in:
2026-07-11 18:28:36 +08:00
parent a2066285cd
commit a913961be5
8 changed files with 1471 additions and 148 deletions

View File

@@ -22,6 +22,17 @@
<view class="splash-screen__core-body"></view>
</view>
</view>
<!-- 启动期进度条 (透明悬浮, 不干扰极光视觉) -->
<view class="splash-screen__progress" v-if="showProgress">
<view class="splash-screen__progress-bar">
<view class="splash-screen__progress-fill" :style="{ width: percent + '%' }"></view>
</view>
<view class="splash-screen__progress-text">
<text class="splash-screen__progress-stage">{{ stageLabel }}</text>
<text class="splash-screen__progress-percent">{{ percent }}%</text>
</view>
</view>
</view>
<!-- #endif -->
</template>
@@ -33,26 +44,70 @@
name: 'splash-screen',
data() {
return {
visible: false,
leaving: false
// 默认 visible=true: 让 splash 组件一渲染就可见, 减少"白屏闪烁"
// created() 钩子会同步 splashState 状态, 如果 splashState 已 hide, 立刻设 false
// 这样既能保证首屏冷启动可见, 又避免挂载到非 splash 路径时误显示
visible: true,
leaving: false,
percent: 0,
stage: 'boot',
showProgress: false,
};
},
mounted() {
if (splashState.isShow()) {
this.visible = true;
computed: {
// 直接从 splashState.STAGES 读取文案, 避免重复维护 STAGE_LABEL
// (splash-state.js 暴露 getStageDef() 避免外部直接读 STAGES)
stageLabel() {
const def = splashState.getStageDef(this.stage)
return def ? def.label : this.stage
},
},
// created (而非 mounted): 提前同步 splashState 状态, 减少"白屏一帧"
created() {
try {
const init = splashState.getProgress()
this.percent = init.percent
this.stage = init.stage
this.showProgress = init.percent > 0
} catch (e) {}
// 同步 splashState 状态: 防止组件挂载在 splash 已 hide 之后误显示
if (!splashState.isShow()) {
this.visible = false
}
},
mounted() {
splashState.notifyMounted();
splashState.on('splash:show', () => {
this.visible = true;
this.leaving = false;
// 注意: 不要在这里再读 getProgress(). percent 不允许后退
// 已通过 created() 同步; emit 进来多是无变化
const init = splashState.getProgress()
if (init.percent > this.percent) this.percent = init.percent
if (init.stage !== this.stage) this.stage = init.stage
this.showProgress = true
});
splashState.on('splash:hide', () => {
this._leave();
splashState.on('splash:hide', (info) => {
// 进度条补完到 100% (视觉上"完成"), 立即 leave
if (this.percent < 100) {
this.percent = 100;
this.stage = 'done';
}
// mode='fast' (隐私弹窗即将出现) → 80ms 短动画让 modal 更快接管视觉
const fast = info && info.mode === 'fast'
this._leave(fast)
});
splashState.onProgress((info) => {
// 去重: percent/stage 没变化就不 setState, 避免空 re-render
if (info.percent !== this.percent) this.percent = info.percent
if (info.stage !== this.stage) this.stage = info.stage
this.showProgress = true;
});
},
beforeDestroy() {
splashState.off('splash:show');
splashState.off('splash:hide');
splashState.offProgress();
splashState.notifyUnmounted();
if (this._leaveTimer) {
clearTimeout(this._leaveTimer);
@@ -60,13 +115,20 @@
}
},
methods: {
_leave() {
/**
* 离开 splash
* - 默认: 480ms fade-out 动画 (视觉柔和过渡)
* - 当 privacy 弹窗即将出现时: 用 80ms 短动画, 让 modal 更快接管视觉
* 否则弹窗与 splash 同时显示会有"双遮罩"感
*/
_leave(fast) {
this.leaving = true;
if (this._leaveTimer) clearTimeout(this._leaveTimer);
const delay = fast ? 80 : 480
this._leaveTimer = setTimeout(() => {
this.visible = false;
this.leaving = false;
}, 480);
}, delay);
}
}
};
@@ -91,6 +153,7 @@
bottom: 0;
z-index: 99999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #ffffff;
@@ -103,6 +166,67 @@
}
}
// 启动期进度条 (悬浮在极光下方)
.splash-screen__progress {
position: absolute;
bottom: 120rpx;
left: 50%;
transform: translateX(-50%);
width: 480rpx;
max-width: 80vw;
display: flex;
flex-direction: column;
gap: 16rpx;
opacity: 0;
animation: progress-fade-in 0.2s ease-out 0.1s forwards;
}
.splash-screen__progress-bar {
position: relative;
height: 6rpx;
background: rgba($accent, 0.08);
border-radius: 3rpx;
overflow: hidden;
}
.splash-screen__progress-fill {
position: absolute;
top: 0;
left: 0;
bottom: 0;
background: linear-gradient(90deg, $accent-light, $accent);
border-radius: 3rpx;
transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 0 12rpx rgba($accent, 0.4);
}
.splash-screen__progress-text {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 22rpx;
color: rgba($accent-deep, 0.65);
letter-spacing: 0.5rpx;
}
.splash-screen__progress-stage {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.splash-screen__progress-percent {
font-variant-numeric: tabular-nums;
font-weight: 500;
color: $accent;
}
@keyframes progress-fade-in {
from { opacity: 0; transform: translateX(-50%) translateY(20rpx); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
// 容器: 480rpx 见方, 居中
.splash-screen__field {
position: relative;
@@ -111,8 +235,8 @@
display: flex;
align-items: center;
justify-content: center;
// 入场: 缩放 + 透明度, 弹性曲线
animation: field-enter 0.8s cubic-bezier(0.34, 1.4, 0.64, 1) both;
// 入场: 缩放 + 透明度, 弹性曲线 (优化: 0.8s -> 0.4s 减少首屏延迟)
animation: field-enter 0.4s cubic-bezier(0.34, 1.4, 0.64, 1) both;
}
// 中心大光晕: 覆盖整个 field, 营造氛围