新增首页骨架屏并优化页面体验

This commit is contained in:
future
2026-06-06 13:25:58 +08:00
committed by liwentao
parent dc7da19aee
commit 5bc31f8936
10 changed files with 694 additions and 216 deletions
@@ -0,0 +1,131 @@
<template>
<SkeletonBase>
<view class="skeleton-banner"></view>
<view class="skeleton-entry">
<view v-for="i in 4" :key="i" class="skeleton-entry-item">
<view class="skeleton-icon"></view>
<view class="skeleton-text"></view>
</view>
</view>
<view class="skeleton-section">
<view class="skeleton-section-title"></view>
<view v-for="i in 3" :key="i" class="skeleton-course-item">
<view class="skeleton-course-img"></view>
<view class="skeleton-course-info">
<view class="skeleton-course-title"></view>
<view class="skeleton-course-desc"></view>
</view>
</view>
</view>
</SkeletonBase>
</template>
<script setup>
import SkeletonBase from './SkeletonBase.vue'
</script>
<style lang="scss" scoped>
.skeleton-banner {
height: 300rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
margin: 20rpx;
border-radius: 20rpx;
}
.skeleton-entry {
display: flex;
justify-content: space-around;
padding: 30rpx 20rpx;
}
.skeleton-entry-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.skeleton-icon {
width: 80rpx;
height: 80rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 40rpx;
}
.skeleton-text {
width: 60rpx;
height: 24rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 12rpx;
}
.skeleton-section {
padding: 20rpx;
}
.skeleton-section-title {
height: 40rpx;
width: 200rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8rpx;
margin-bottom: 24rpx;
}
.skeleton-course-item {
display: flex;
gap: 20rpx;
margin-bottom: 24rpx;
padding: 20rpx;
background: #fff;
border-radius: 20rpx;
}
.skeleton-course-img {
width: 160rpx;
height: 160rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 16rpx;
}
.skeleton-course-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.skeleton-course-title {
height: 36rpx;
width: 80%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8rpx;
}
.skeleton-course-desc {
height: 28rpx;
width: 60%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8rpx;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
+191 -24
View File
@@ -1,14 +1,15 @@
<!-- components/TabBar.vue -->
<template>
<view class="tab-bar">
<view v-if="shouldShowTabBar" class="tab-bar">
<view
v-for="(tab, index) in tabs"
:key="tab.path"
:class="['tab-item', { active: currentIndex === index }]"
:class="['tab-item', { active: currentActiveIndex === index }]"
hover-class="tab-item--hover"
@tap="onTabTap(index)"
@tap.stop="onTabTap(index)"
>
<image
:src="currentIndex === index ? tab.iconActive : tab.icon"
:src="currentActiveIndex === index ? tab.iconActive : tab.icon"
mode="aspectFit"
class="tab-icon"
/>
@@ -18,24 +19,149 @@
</template>
<script setup>
import { computed, ref } from 'vue'
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import {
PAGE,
TAB_ROUTES,
getCurrentRoutePath,
getTabIndexByRoute,
switchToTab
getTabIndexByRoute
} from '@/common/constants/routes.js'
const props = defineProps({
/** 当前 Tab 索引,由 Tab 页传入以保证高亮准确 */
active: {
type: Number,
default: -1
}
active: { type: Number, default: -1 },
activeTab: { type: Number, default: -1 }
})
const tapping = ref(false)
const emit = defineEmits(['update:active', 'tab-change'])
// 当前激活的索引 - 默认从路由获取
const currentActiveIndex = ref(-1)
// 是否需要显示 TabBar
const shouldShowTabBar = ref(true)
// 不需要显示 TabBar 的页面路径列表(注意:不要带开头的 /)
const HIDE_TABBAR_PAGES = [
'pages/memberInfo/courseList', // 预约课程
'pages/memberInfo/courseDetail', // 课程详情
'pages/memberInfo/booking', // 我的预约
'pages/memberInfo/bodyTestReport', // 体测报告
'pages/groupCourse/list', // 团课列表
'pages/groupCourse/detail', // 团课详情
'pages/searchCourse/searchCourse', // 搜索课程
'pages/checkIn/checkIn', // 会员签到
'pages/memberInfo/myCourses', // 我的课程
'pages/memberInfo/coupons', // 我的优惠券
'pages/memberInfo/points', // 我的积分
'pages/memberInfo/pointsMall', // 积分商城
'pages/memberInfo/referral', // 邀请好友
'pages/memberInfo/userInfo', // 个人信息
'pages/memberInfo/memberCard', // 我的会员卡
]
// 从路由获取当前激活的 tab
function getActiveIndexFromRoute() {
const routePath = getCurrentRoutePath()
const index = getTabIndexByRoute(routePath)
console.log('从路由获取索引:', routePath, '->', index)
return index >= 0 ? index : 0
}
// 同步激活状态(高优先级:路由 > props)
function syncActiveState() {
// 优先从路由获取(最准确)
const routeIndex = getActiveIndexFromRoute()
if (routeIndex >= 0) {
currentActiveIndex.value = routeIndex
return
}
// 其次使用 props
if (props.active >= 0) {
currentActiveIndex.value = props.active
} else if (props.activeTab >= 0) {
currentActiveIndex.value = props.activeTab
} else {
currentActiveIndex.value = 0
}
}
// 检查当前页面是否需要隐藏 TabBar
function checkShouldShow() {
let routePath = getCurrentRoutePath()
// 标准化路径:去掉开头的 /
if (routePath.startsWith('/')) {
routePath = routePath.slice(1)
}
// 去掉查询参数(?后面的内容)
if (routePath.includes('?')) {
routePath = routePath.split('?')[0]
}
// 检查是否在隐藏列表中
const shouldHide = HIDE_TABBAR_PAGES.includes(routePath)
shouldShowTabBar.value = !shouldHide
console.log('=== TabBar 显示控制 ===')
console.log('原始路径:', getCurrentRoutePath())
console.log('标准化路径:', routePath)
console.log('是否隐藏:', shouldHide)
console.log('是否显示 TabBar:', shouldShowTabBar.value)
}
// 监听路由变化(页面切换时自动同步)
let routeWatcher = null
let appRouteCallback = null
onMounted(() => {
// 初始同步
syncActiveState()
checkShouldShow()
// #ifdef APP-PLUS
// App 端:监听页面显示
routeWatcher = setInterval(() => {
syncActiveState()
checkShouldShow()
}, 300)
// #endif
// #ifdef MP-WEIXIN
// 小程序端:监听路由变化
if (typeof uni.onAppRoute === 'function') {
appRouteCallback = () => {
setTimeout(() => {
syncActiveState()
checkShouldShow()
}, 100)
}
uni.onAppRoute(appRouteCallback)
}
// #endif
})
onBeforeUnmount(() => {
// #ifdef APP-PLUS
if (routeWatcher) {
clearInterval(routeWatcher)
}
// #endif
// #ifdef MP-WEIXIN
if (appRouteCallback && typeof uni.offAppRoute === 'function') {
uni.offAppRoute(appRouteCallback)
}
// #endif
})
// 监听 props 变化
watch(() => props.active, () => {
const routeIndex = getActiveIndexFromRoute()
if (routeIndex !== currentActiveIndex.value) {
syncActiveState()
}
})
const tabs = [
{
@@ -70,18 +196,54 @@ const tabs = [
}
]
const currentIndex = computed(() => {
if (props.active >= 0) return props.active
return getTabIndexByRoute(getCurrentRoutePath())
})
let isSwitching = false
function onTabTap(index) {
if (tapping.value || index === currentIndex.value) return
tapping.value = true
switchToTab(TAB_ROUTES[index])
setTimeout(() => {
tapping.value = false
}, 350)
if (isSwitching) return
const targetPath = TAB_ROUTES[index]
const currentPath = TAB_ROUTES[currentActiveIndex.value]
if (targetPath === currentPath) return
console.log('Tab 点击:', index, targetPath)
// 1. 立即更新 UI 高亮
currentActiveIndex.value = index
// 2. 通知父组件
emit('update:active', index)
emit('tab-change', index)
// 3. 显示 loading(可选)
let timer = setTimeout(() => {
uni.showLoading({ title: '加载中...', mask: true })
}, 50)
isSwitching = true
// 4. 执行跳转
uni.switchTab({
url: targetPath,
success: () => {
console.log('switchTab 成功:', targetPath)
},
fail: (err) => {
console.error('switchTab 失败:', err)
// 降级
uni.reLaunch({ url: targetPath })
},
complete: () => {
clearTimeout(timer)
uni.hideLoading()
setTimeout(() => {
isSwitching = false
// 跳转完成后,再次同步确保高亮正确
syncActiveState()
checkShouldShow()
}, 100)
}
})
}
</script>
@@ -109,6 +271,11 @@ function onTabTap(index) {
align-items: center;
gap: 8rpx;
padding: 12rpx 24rpx;
transition: all 0.1s ease;
}
.tab-item:active {
transform: scale(0.95);
}
.tab-icon {
@@ -125,4 +292,4 @@ function onTabTap(index) {
color: #f97316;
font-weight: 600;
}
</style>
</style>