Files
gym-manage/gym-manage-uniapp/components/index/QuickEntry.vue
T

442 lines
11 KiB
Vue

<template>
<view class="quick-entry">
<view
v-for="(item, index) in entries"
:key="index"
class="entry-item"
@tap="handleEntryClick(item)"
>
<view :class="['entry-icon', { accent: item.accent }]">
<image :src="item.icon" mode="aspectFit" class="icon-img" />
<view v-if="item.title === '消息' && hasUnread" class="badge-dot"></view>
</view>
<text class="entry-title">{{ item.title }}</text>
<text class="entry-desc">{{ item.desc }}</text>
</view>
<view v-if="showCheckInMenu" class="checkin-menu-overlay" @tap="showCheckInMenu = false">
<view class="checkin-menu" @tap.stop>
<view class="menu-item" @tap="handleStoreCheckIn">
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png" mode="aspectFit" class="menu-icon" />
<text class="menu-text">到店签到</text>
</view>
<view class="menu-item" @tap="handleGroupCourseCheckIn">
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png" mode="aspectFit" class="menu-icon" />
<text class="menu-text">团课签到</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { getUnreadMessageCount } from '@/api/main.js'
const unreadCount = ref(0)
const showCheckInMenu = ref(false)
const TEST_MODE = false
// 是否有未读消息(用于显示红点)
const hasUnread = computed(() => unreadCount.value > 0)
const loadUnreadCount = async () => {
if (TEST_MODE) {
unreadCount.value = 3
return
}
const token = uni.getStorageSync('token')
if (!token) {
console.log('[QuickEntry] token 不存在,跳过请求')
unreadCount.value = 0
return
}
try {
const userId = uni.getStorageSync('userId') || 1
console.log('[QuickEntry] 请求未读消息数量, userId:', userId)
const res = await getUnreadMessageCount(userId)
console.log('[QuickEntry] 未读消息数量响应数据:', res)
console.log('[QuickEntry] 响应完整类型:', typeof res, Object.prototype.toString.call(res))
let count = 0
// 兼容多种响应格式
if (typeof res === 'number') {
count = res
console.log('[QuickEntry] 使用直接数字格式')
} else if (typeof res === 'string' && res.trim() !== '') {
// 处理字符串数字,如 "20"
count = parseInt(res, 10)
console.log('[QuickEntry] 使用字符串转数字格式:', count)
} else if (res !== null && typeof res === 'object') {
if (typeof res.count === 'number') {
count = res.count
console.log('[QuickEntry] 使用 res.count')
} else if (res.data !== undefined) {
if (typeof res.data === 'number') {
count = res.data
console.log('[QuickEntry] 使用 res.data')
} else if (typeof res.data?.count === 'number') {
count = res.data.count
console.log('[QuickEntry] 使用 res.data.count')
} else if (Array.isArray(res.data?.content)) {
count = res.data.content.length
console.log('[QuickEntry] 使用 res.data.content.length')
} else if (Array.isArray(res.content)) {
count = res.content.length
console.log('[QuickEntry] 使用 res.content.length')
} else if (typeof res.data?.total === 'number') {
count = res.data.total
console.log('[QuickEntry] 使用 res.data.total')
} else if (typeof res.total === 'number') {
count = res.total
console.log('[QuickEntry] 使用 res.total')
}
} else if (Array.isArray(res.content)) {
count = res.content.length
console.log('[QuickEntry] 使用 res.content.length')
} else if (typeof res.total === 'number') {
count = res.total
console.log('[QuickEntry] 使用 res.total')
}
} else {
console.log('[QuickEntry] 无法解析响应格式,res:', res)
}
unreadCount.value = count
uni.setStorageSync('unreadMessageCount', count)
console.log('[QuickEntry] 最终未读数量:', count, 'hasUnread:', hasUnread.value)
} catch (e) {
console.error('[QuickEntry] 获取未读消息数失败:', e)
try {
const count = uni.getStorageSync('unreadMessageCount')
unreadCount.value = count || 0
} catch (e2) {
unreadCount.value = 0
}
}
}
const handleEntryClick = (item) => {
if (item.title === '消息' || item.title === '健身数据') {
if (TEST_MODE) {
uni.showLoading({
title: '加载中...',
mask: true
})
setTimeout(() => {
uni.hideLoading()
if (item.isTabBar) {
uni.switchTab({ url: item.path })
} else {
uni.navigateTo({ url: item.path })
}
}, 300)
return
}
const token = uni.getStorageSync('token')
if (!token) {
uni.showModal({
title: '请登录',
content: '是否跳转到登录页面?',
confirmText: '确定',
cancelText: '取消',
confirmColor: '#FF6B35',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/login/login'
})
}
}
})
return
}
uni.showLoading({
title: '加载中...',
mask: true
})
setTimeout(() => {
uni.hideLoading()
if (item.title === '消息') {
uni.navigateTo({
url: `${item.path}?unreadCount=${unreadCount.value}`
})
} else if (item.isTabBar) {
uni.switchTab({ url: item.path })
} else {
uni.navigateTo({ url: item.path })
}
}, 300)
} else if (item.title === '签到') {
const token = uni.getStorageSync('token')
if (!token) {
uni.showModal({
title: '请登录',
content: '是否跳转到登录页面?',
confirmText: '确定',
cancelText: '取消',
confirmColor: '#FF6B35',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/login/login'
})
}
}
})
return
}
showCheckInMenu.value = !showCheckInMenu.value
} else if (item.path) {
// tabbar 页面使用 switchTab
if (item.isTabBar) {
uni.switchTab({
url: item.path
})
} else {
uni.navigateTo({
url: item.path
})
}
}
}
const handleStoreCheckIn = () => {
showCheckInMenu.value = false
uni.navigateTo({
url: '/pages/checkIn/checkIn'
})
}
const handleGroupCourseCheckIn = () => {
showCheckInMenu.value = false
uni.scanCode({
onlyFromCamera: true,
success: (res) => {
console.log('扫码结果:', res)
uni.showToast({
title: '扫码成功',
icon: 'success'
})
},
fail: (err) => {
console.error('扫码失败:', err)
uni.showToast({
title: '扫码失败',
icon: 'none'
})
}
})
}
onMounted(() => {
loadUnreadCount()
})
uni.$on('pageShow', () => {
loadUnreadCount()
})
const entries = [
{
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png',
title: '找课程',
desc: '精品课程',
accent: false,
path: "/pages/searchCourse/searchCourse"
},
{
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/plan.png',
title: '训练计划',
desc: '个性定制',
accent: true,
path: "/pages/train/index",
isTabBar: true
},
{
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/data.png',
title: '健身数据',
desc: '记录分析',
accent: false,
path: "/pages/memberInfo/bodyTestTrend"
},
{
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/message.png',
title: '消息',
desc: '通知消息',
accent: true,
path: "/pages/message/index"
},
{
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png',
title: '签到',
desc: '打卡签到',
accent: false,
path: "/pages/checkIn/checkIn"
}
]
</script>
<style lang="scss" scoped>
.quick-entry {
display: flex;
justify-content: space-between;
padding: 32rpx 24rpx;
background: rgba(255, 255, 255, 0.55);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
margin: 24rpx;
border-radius: 28rpx;
box-shadow: 0 8rpx 32rpx var(--shadow-blue-light);
border: 1rpx solid rgba(255, 255, 255, 0.7);
position: relative;
z-index: 3;
}
.entry-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.entry-icon {
width: 104rpx;
height: 104rpx;
border-radius: 24rpx;
background: rgba(130, 220, 130, 0.9);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
box-shadow: 0 6rpx 20rpx rgba(130, 220, 130, 0.35);
position: relative;
}
.badge-dot {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 16rpx;
height: 16rpx;
background: linear-gradient(135deg, #FF4757, #FF2D55);
border-radius: 50%;
box-shadow: 0 2rpx 8rpx rgba(255, 71, 87, 0.5);
z-index: 99;
animation: pulse 2s infinite;
}
.icon-img {
width: 52rpx;
height: 52rpx;
}
.entry-icon.accent {
background: rgba(130, 220, 130, 0.9);
}
.entry-title {
font-size: 26rpx;
font-weight: 600;
color: #2D4A5A;
margin-bottom: 4rpx;
}
.entry-desc {
font-size: 22rpx;
color: var(--tabbar-text-inactive);
}
.checkin-menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: flex-start;
justify-content: flex-end;
padding-top: 280rpx;
padding-right: 60rpx;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.checkin-menu {
position: absolute;
right: 0;
top: 140rpx;
background: white;
border-radius: 24rpx;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
padding: 16rpx;
min-width: 320rpx;
animation: slideDown 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
&::before {
content: '';
position: absolute;
top: -24rpx;
right: 60rpx;
width: 0;
height: 0;
border-left: 24rpx solid transparent;
border-right: 24rpx solid transparent;
border-bottom: 24rpx solid white;
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-40rpx) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.menu-item {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
border-radius: 16rpx;
transition: all 0.2s ease;
&:active {
background: rgba(0, 0, 0, 0.04);
transform: scale(0.98);
}
& + .menu-item {
margin-top: 8rpx;
}
}
.menu-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.menu-text {
font-size: 30rpx;
font-weight: 600;
color: #2D4A5A;
}
</style>