442 lines
11 KiB
Vue
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> |