- 新增隐私管理器核心逻辑,支持URL路由规则匹配、远程patch签名校验与融合 - 实现启动期资源等待机制,确保弹窗在i18n、主题配置就绪后触发 - 新增启动进度条组件,展示启动阶段进度与状态 - 重构隐私弹窗组件生命周期,修复事件丢失问题并支持规则动态重评估 - 适配多端平台隐私检查逻辑,支持微信小程序、鸿蒙元服务与H5 - 支持uniacid租户隔离的持久化规则存储
439 lines
11 KiB
Vue
439 lines
11 KiB
Vue
<template>
|
|
<!-- #ifndef APP-PLUS -->
|
|
<view v-if="visible" class="splash-screen" :class="{ 'splash-screen--leaving': leaving }">
|
|
<view class="splash-screen__field">
|
|
<!-- 中心大光晕: 整片柔光铺底 -->
|
|
<view class="splash-screen__aura"></view>
|
|
|
|
<!-- 三圈轨道: 由内到外, 描边极淡 -->
|
|
<view class="splash-screen__orbit splash-screen__orbit--1">
|
|
<view class="splash-screen__particle splash-screen__particle--1"></view>
|
|
</view>
|
|
<view class="splash-screen__orbit splash-screen__orbit--2">
|
|
<view class="splash-screen__particle splash-screen__particle--2"></view>
|
|
</view>
|
|
<view class="splash-screen__orbit splash-screen__orbit--3">
|
|
<view class="splash-screen__particle splash-screen__particle--3"></view>
|
|
</view>
|
|
|
|
<!-- 中心星核: 渐变球体 + 内反光 + 外发光 -->
|
|
<view class="splash-screen__core">
|
|
<view class="splash-screen__core-halo"></view>
|
|
<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>
|
|
|
|
<script>
|
|
import splashState from '@/common/js/splash-state.js';
|
|
|
|
export default {
|
|
name: 'splash-screen',
|
|
data() {
|
|
return {
|
|
// 默认 visible=true: 让 splash 组件一渲染就可见, 减少"白屏闪烁"
|
|
// created() 钩子会同步 splashState 状态, 如果 splashState 已 hide, 立刻设 false
|
|
// 这样既能保证首屏冷启动可见, 又避免挂载到非 splash 路径时误显示
|
|
visible: true,
|
|
leaving: false,
|
|
percent: 0,
|
|
stage: 'boot',
|
|
showProgress: false,
|
|
};
|
|
},
|
|
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', (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);
|
|
this._leaveTimer = null;
|
|
}
|
|
},
|
|
methods: {
|
|
/**
|
|
* 离开 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;
|
|
}, delay);
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
// ============================================================
|
|
// 启动屏 — 极光脉冲
|
|
// 中心柔光球呼吸, 三圈粒子轨道旋转, 整体白底 + 蓝紫辉光
|
|
// 单一焦点 / 对称结构 / 黄金比例节奏, 跨平台一致
|
|
// ============================================================
|
|
|
|
$accent: #6366F1; // 主色: 靛蓝
|
|
$accent-light: #A5B4FC; // 亮部
|
|
$accent-deep: #4338CA; // 暗部
|
|
|
|
.splash-screen {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 99999;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #ffffff;
|
|
overflow: hidden;
|
|
opacity: 1;
|
|
transition: opacity 0.45s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
&--leaving {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
// 启动期进度条 (悬浮在极光下方)
|
|
.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;
|
|
width: 480rpx;
|
|
height: 480rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
// 入场: 缩放 + 透明度, 弹性曲线 (优化: 0.8s -> 0.4s 减少首屏延迟)
|
|
animation: field-enter 0.4s cubic-bezier(0.34, 1.4, 0.64, 1) both;
|
|
}
|
|
|
|
// 中心大光晕: 覆盖整个 field, 营造氛围
|
|
.splash-screen__aura {
|
|
position: absolute;
|
|
width: 480rpx;
|
|
height: 480rpx;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle,
|
|
rgba($accent-light, 0.35) 0%,
|
|
rgba($accent, 0.18) 25%,
|
|
rgba($accent, 0.05) 50%,
|
|
rgba($accent, 0) 75%);
|
|
box-shadow: 0 0 200rpx rgba($accent, 0.15);
|
|
animation: aura-breathe 3.6s ease-in-out infinite;
|
|
}
|
|
|
|
// 中心星核
|
|
.splash-screen__core {
|
|
position: relative;
|
|
width: 36rpx;
|
|
height: 36rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 2;
|
|
animation: core-pulse 2.4s ease-in-out infinite;
|
|
}
|
|
|
|
// 星核外层光晕: 跟随呼吸
|
|
.splash-screen__core-halo {
|
|
position: absolute;
|
|
width: 120rpx;
|
|
height: 120rpx;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle,
|
|
rgba($accent, 0.5) 0%,
|
|
rgba($accent, 0) 60%);
|
|
animation: halo-expand 2.4s ease-in-out infinite;
|
|
}
|
|
|
|
// 星核本体: 渐变球体
|
|
.splash-screen__core-body {
|
|
position: relative;
|
|
width: 36rpx;
|
|
height: 36rpx;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 35% 30%,
|
|
#EEF2FF 0%,
|
|
$accent-light 35%,
|
|
$accent 70%,
|
|
$accent-deep 100%);
|
|
box-shadow:
|
|
0 0 24rpx rgba($accent, 0.6),
|
|
0 0 48rpx rgba($accent, 0.3),
|
|
inset -2rpx -3rpx 6rpx rgba($accent-deep, 0.5),
|
|
inset 2rpx 2rpx 4rpx rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
// 轨道
|
|
.splash-screen__orbit {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
border-radius: 50%;
|
|
pointer-events: none;
|
|
transform-origin: center;
|
|
|
|
&--1 {
|
|
width: 160rpx;
|
|
height: 160rpx;
|
|
margin-top: -80rpx;
|
|
margin-left: -80rpx;
|
|
border: 1rpx solid rgba($accent, 0.18);
|
|
animation: orbit-rotate 6s linear infinite;
|
|
}
|
|
|
|
&--2 {
|
|
width: 260rpx;
|
|
height: 260rpx;
|
|
margin-top: -130rpx;
|
|
margin-left: -130rpx;
|
|
border: 1rpx solid rgba($accent, 0.1);
|
|
animation: orbit-rotate 10s linear infinite reverse;
|
|
}
|
|
|
|
&--3 {
|
|
width: 360rpx;
|
|
height: 360rpx;
|
|
margin-top: -180rpx;
|
|
margin-left: -180rpx;
|
|
border: 1rpx solid rgba($accent, 0.06);
|
|
animation: orbit-rotate 16s linear infinite;
|
|
}
|
|
}
|
|
|
|
// 粒子: 沿轨道运动, 起始位置在轨道顶部
|
|
.splash-screen__particle {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 30% 30%,
|
|
#EEF2FF 0%,
|
|
$accent-light 40%,
|
|
$accent 100%);
|
|
|
|
&--1 {
|
|
width: 18rpx;
|
|
height: 18rpx;
|
|
margin-top: -9rpx;
|
|
margin-left: -9rpx;
|
|
transform: translateY(-80rpx);
|
|
box-shadow:
|
|
0 0 24rpx rgba($accent, 0.7),
|
|
0 0 8rpx rgba(255, 255, 255, 0.6);
|
|
}
|
|
|
|
&--2 {
|
|
width: 14rpx;
|
|
height: 14rpx;
|
|
margin-top: -7rpx;
|
|
margin-left: -7rpx;
|
|
transform: translateY(-130rpx);
|
|
box-shadow:
|
|
0 0 18rpx rgba($accent, 0.5),
|
|
0 0 4rpx rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
&--3 {
|
|
width: 10rpx;
|
|
height: 10rpx;
|
|
margin-top: -5rpx;
|
|
margin-left: -5rpx;
|
|
transform: translateY(-180rpx);
|
|
box-shadow: 0 0 14rpx rgba($accent, 0.4);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 动画
|
|
// ============================================================
|
|
|
|
// 容器入场
|
|
@keyframes field-enter {
|
|
0% {
|
|
opacity: 0;
|
|
transform: scale(0.85);
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
// 中心光晕呼吸
|
|
@keyframes aura-breathe {
|
|
0%, 100% {
|
|
transform: scale(1);
|
|
opacity: 0.85;
|
|
}
|
|
50% {
|
|
transform: scale(1.08);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
// 中心星核呼吸
|
|
@keyframes core-pulse {
|
|
0%, 100% {
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
transform: scale(1.1);
|
|
}
|
|
}
|
|
|
|
// 星核光晕扩散
|
|
@keyframes halo-expand {
|
|
0%, 100% {
|
|
transform: scale(1);
|
|
opacity: 0.7;
|
|
}
|
|
50% {
|
|
transform: scale(1.5);
|
|
opacity: 0.3;
|
|
}
|
|
}
|
|
|
|
// 轨道旋转
|
|
@keyframes orbit-rotate {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|