feat: 新增统一启动屏组件,优化跨平台加载体验

1. 新增splash-screen全局启动屏组件,适配非APP平台的加载过渡
2. 新增splash-state状态管理模块,统一处理启动屏的显示隐藏、软隐藏时序、兜底超时逻辑
3. 重构App.vue和main.js的加载逻辑,替换原有uni.showLoading方案
4. 在首页和空页面挂载启动屏组件,适配不同页面场景
5. 兼容H5平台降级处理,解决main.js挂载前无法渲染组件的问题
This commit is contained in:
2026-07-01 15:36:55 +08:00
parent c0220492a2
commit 5ae4bbcbd3
6 changed files with 420 additions and 19 deletions

View File

@@ -0,0 +1,178 @@
<template>
<!-- #ifndef APP-PLUS -->
<view v-if="visible" class="splash-screen" :class="{ 'splash-screen--leaving': leaving }">
<view class="splash-screen__bg"></view>
<view class="splash-screen__content">
<view class="splash-screen__logo">
<view class="splash-screen__logo-inner">S</view>
</view>
<view class="splash-screen__name">Shop</view>
<view class="splash-screen__loader">
<view class="splash-screen__dot"></view>
<view class="splash-screen__dot"></view>
<view class="splash-screen__dot"></view>
</view>
</view>
</view>
<!-- #endif -->
</template>
<script>
import splashState from '@/common/js/splash-state.js';
export default {
name: 'splash-screen',
data() {
return {
visible: false,
leaving: false
};
},
mounted() {
// 1) 启动时同步读取状态 (防止 mount 前已经 hide 的情况)
if (splashState.isShow()) {
this.visible = true;
}
// 2) 通知 state 模块: 组件已 mount
// 如果此时 splashState 有 _pendingHide (软隐藏), 会触发真关
splashState.notifyMounted();
// 3) 监听后续事件
splashState.on('splash:show', () => {
this.visible = true;
this.leaving = false;
});
splashState.on('splash:hide', () => {
this._leave();
});
},
beforeDestroy() {
splashState.off('splash:show');
splashState.off('splash:hide');
splashState.notifyUnmounted();
if (this._leaveTimer) {
clearTimeout(this._leaveTimer);
this._leaveTimer = null;
}
},
methods: {
_leave() {
// 触发淡出动画,动画结束再 unmount
this.leaving = true;
if (this._leaveTimer) clearTimeout(this._leaveTimer);
this._leaveTimer = setTimeout(() => {
this.visible = false;
this.leaving = false;
}, 320);
}
}
};
</script>
<style lang="scss" scoped>
.splash-screen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 0.3s ease;
&--leaving {
opacity: 0;
}
}
.splash-screen__bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, #ffffff 0%, #f7f7f7 100%);
}
.splash-screen__content {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.splash-screen__logo {
width: 96rpx;
height: 96rpx;
border-radius: 24rpx;
background: #FF0036;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(255, 0, 54, 0.18);
animation: splash-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
.splash-screen__logo-inner {
color: #fff;
font-size: 56rpx;
font-weight: 700;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
}
.splash-screen__name {
margin-top: 28rpx;
font-size: 32rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
.splash-screen__loader {
margin-top: 48rpx;
display: flex;
gap: 12rpx;
}
.splash-screen__dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #FF0036;
opacity: 0.4;
animation: splash-bounce 1.2s ease-in-out infinite;
&:nth-child(2) {
animation-delay: 0.15s;
}
&:nth-child(3) {
animation-delay: 0.3s;
}
}
@keyframes splash-pop {
0% {
transform: scale(0.6);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes splash-bounce {
0%, 80%, 100% {
transform: scale(0.7);
opacity: 0.4;
}
40% {
transform: scale(1);
opacity: 1;
}
}
</style>