limo-platform/client/src/pages/index/index.vue
2025-07-11 18:28:43 +08:00

835 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="limo-mobile">
<!-- 首页内容 -->
<view v-if="currentPage === 'home'">
<!-- Header with logo and energy button -->
<view class="header">
<view class="header-content">
<image :src="logoSrc" class="logo" mode="aspectFit" />
<!-- <view class="energy-button" @click="goToMSpace">
<text class="energy-icon"></text>
</view> -->
</view>
<!-- Navigation Tabs -->
<view class="nav-tabs">
<view class="tab-item active">
<text class="tab-text">发现</text>
<view class="tab-underline"></view>
</view>
<view class="tab-item">
<text class="tab-text">关注</text>
</view>
</view>
</view>
<!-- Sports Categories -->
<view class="sports-section">
<view class="sports-container">
<view class="sport-item all-category" :class="{ active: selectedSport === 'all' }" @tap="selectSport('all')">
<view class="category-content">
<text class="category-text">全部</text>
<text class="category-text">项目</text>
</view>
</view>
<view class="sport-item" :class="{ active: selectedSport === 'basketball' }" @tap="selectSport('basketball')">
<view class="sport-icon-container basketball">
<text class="sport-emoji">🏀</text>
</view>
<text class="sport-label">篮球</text>
</view>
<view class="sport-item" :class="{ active: selectedSport === 'football' }" @tap="selectSport('football')">
<view class="sport-icon-container football">
<text class="sport-emoji"></text>
</view>
<text class="sport-label">足球</text>
</view>
<view class="sport-item" :class="{ active: selectedSport === 'tennis' }" @tap="selectSport('tennis')">
<view class="sport-icon-container tennis">
<text class="sport-emoji">🎾</text>
</view>
<text class="sport-label">网球</text>
</view>
</view>
</view>
<!-- Location Bar -->
<view class="location-bar">
<view class="location-content">
<text class="location-pin">📍</text>
<text class="location-text" :class="{ 'location-loading': locationLoading }">{{ currentLocation.address }}</text>
<text class="location-switch" @tap="switchLocation">{{ locationLoading ? '定位中' : (currentLocation.address.includes('请选择') ? '选择' : '切换') }}</text>
</view>
</view>
<!-- 根据定位状态显示不同内容 -->
<!-- 定位中状态 -->
<view v-if="locationLoading" class="location-status-section">
<view class="status-container">
<view class="loading-spinner"></view>
<text class="status-title">正在定位</text>
<text class="status-subtitle">获取您的位置信息为您推荐附近场馆</text>
</view>
</view>
<!-- 选择位置状态 -->
<view v-else-if="locationError" class="location-status-section">
<view class="status-container">
<text class="status-icon">📍</text>
<text class="status-title">选择您的位置</text>
<text class="status-subtitle">在地图上选择位置为您推荐附近的运动场馆</text>
<view class="retry-button" @tap="chooseLocationOnMap">
<text class="retry-text">在地图上选择</text>
</view>
</view>
</view>
<!-- 定位成功显示场馆列表 -->
<view v-else-if="locationReady" class="venue-section">
<!-- Timeline -->
<view class="timeline-container">
<view class="timeline-line"></view>
</view>
<!-- First Venue -->
<view class="venue-card">
<view class="timeline-dot active"></view>
<view class="venue-meta">
<text class="distance">120km</text>
<text class="separator"></text>
<view class="update-time">
<text class="clock-icon">🕐</text>
<text class="time-text">2分钟前更新</text>
</view>
</view>
<view class="venue-content">
<view class="venue-image">
<view class="image-placeholder ronin-bg">
<text class="placeholder-text">RONIN篮球馆</text>
</view>
<view class="play-overlay" @tap="playVideo">
<view class="play-button">
<text class="play-icon"></text>
</view>
</view>
<view class="live-badge">
<view class="live-dot"></view>
<text class="live-text">直播中</text>
</view>
</view>
<view class="venue-info">
<view class="venue-header">
<view class="venue-logo ronin-logo">
<image :src="roninLogoSrc" class="venue-logo-image" mode="aspectFit" />
</view>
<view class="venue-details">
<text class="venue-name">RONIN黄金篮球馆</text>
<text class="venue-address">杭州拱墅区黑马路124号肯德基对面</text>
</view>
</view>
<view class="venue-action">
<view class="zone-button" @tap="enterZone('ronin')">
<text class="button-text">进入ZONE</text>
</view>
</view>
</view>
</view>
</view>
<!-- Second Venue -->
<view class="venue-card">
<view class="timeline-dot"></view>
<view class="venue-meta">
<text class="distance">140km</text>
<text class="separator"></text>
<view class="update-time">
<text class="clock-icon">🕐</text>
<text class="time-text">5分钟前更新</text>
</view>
</view>
<view class="venue-content">
<view class="venue-image">
<view class="image-placeholder panda-bg">
<text class="placeholder-text">Panda运动俱乐部</text>
</view>
<view class="play-overlay" @tap="playVideo">
<view class="play-button">
<text class="play-icon"></text>
</view>
</view>
<view class="live-badge">
<view class="live-dot"></view>
<text class="live-text">直播中</text>
</view>
</view>
<view class="venue-info">
<view class="venue-header">
<view class="venue-logo panda-logo">
<text class="logo-text">P</text>
</view>
<view class="venue-details">
<text class="venue-name">Panda惊怒熊猫运动俱乐部</text>
<text class="venue-address">杭州拱墅区黑马路124号肯德基对面</text>
</view>
</view>
<view class="venue-action">
<view class="zone-button" @tap="enterZone('panda')">
<text class="button-text">进入ZONE</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 我的页面内容 -->
<view v-if="currentPage === 'profile'" class="profile-content">
<!-- User Profile Section -->
<view class="profile-section">
<view class="profile-card">
<view class="profile-header-info">
<view class="avatar-container">
<view class="avatar">
<text class="avatar-initial">U</text>
</view>
<view class="online-status"></view>
</view>
<view class="profile-info">
<view class="profile-name-row">
<text class="profile-name">用户昵称</text>
<view class="edit-icon" @tap="editProfile">
<text class="edit-text"></text>
</view>
</view>
<text class="profile-id">ID: 123456789</text>
<view class="profile-stats">
<text class="stat-text">积分 2,580</text>
<view class="stat-divider"></view>
<text class="stat-text">等级 LV.8</text>
</view>
</view>
</view>
<view class="member-card-button" @tap="openMemberBenefits">
<view class="card-background">
<view class="card-pattern"></view>
<view class="card-shine"></view>
</view>
<view class="card-content">
<view class="card-left">
<view class="card-header">
<text class="card-title">LIMO</text>
<text class="card-subtitle">会员卡</text>
</view>
<view class="card-info">
<text class="member-level">VIP黄金会员</text>
<text class="member-expiry">有效期至 2025.12.31</text>
</view>
</view>
<view class="card-arrow">
<text class="arrow-icon"></text>
</view>
</view>
</view>
</view>
</view>
<!-- Stats Cards -->
<view class="stats-section">
<view class="stats-grid">
<view class="stat-card" @tap="goToHistory">
<text class="stat-number">12</text>
<text class="stat-label">观看数量</text>
</view>
<view class="stat-card" @tap="goToFavorites">
<text class="stat-number">5</text>
<text class="stat-label">收藏场馆</text>
</view>
</view>
</view>
<!-- Menu Items -->
<view class="menu-section">
<view class="menu-card">
<view class="menu-item" @tap="goToFavorites">
<view class="menu-content">
<text class="menu-title">我的收藏</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @tap="goToHistory">
<view class="menu-content">
<text class="menu-title">观看历史</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @tap="goToNotifications">
<view class="menu-content">
<text class="menu-title">消息通知</text>
</view>
<view class="menu-right">
<view class="notification-dot"></view>
<text class="menu-arrow"></text>
</view>
</view>
<view class="menu-item" @tap="goToOrders">
<view class="menu-content">
<text class="menu-title">我的订单</text>
</view>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<!-- Recent Activity -->
<view class="activity-section">
<view class="activity-card">
<view class="activity-header">
<view class="activity-indicator"></view>
<text class="activity-title">最近活动</text>
</view>
<view class="activity-list">
<view class="activity-item">
<view class="activity-content">
<text class="activity-title-text">观看了 RONIN黄金篮球馆</text>
<text class="activity-subtitle">2小时前 · 观看时长 45分钟</text>
</view>
</view>
<view class="activity-item">
<view class="activity-content">
<text class="activity-title-text">收藏了 Panda惊怒熊猫运动俱乐部</text>
<text class="activity-subtitle">1天前</text>
</view>
</view>
</view>
<view class="activity-more-link" @tap="goToActivityHistory">
<text class="more-link-text">查看历史活动</text>
</view>
</view>
</view>
</view>
<!-- Bottom Spacing -->
<view class="bottom-spacing"></view>
<!-- Bottom Navigation -->
<view class="bottom-nav">
<view class="nav-item" :class="{ active: currentPage === 'home' }" @tap="goHome">
<view class="nav-icon-container" :class="{ 'home-active': currentPage === 'home' }">
<image :src="homeIcon" class="nav-icon-svg" mode="aspectFit" />
</view>
<text class="nav-label" :class="{ active: currentPage === 'home' }">首页</text>
</view>
<view class="nav-item" :class="{ active: currentPage === 'profile' }" @tap="goProfile">
<view class="nav-icon-container" :class="{ 'profile-active': currentPage === 'profile' }">
<image :src="userIcon" class="nav-icon-svg" mode="aspectFit" />
</view>
<text class="nav-label" :class="{ active: currentPage === 'profile' }">我的</text>
</view>
</view>
<!-- 授权弹窗 -->
<AuthModal
:visible="authModalVisible"
:auth-type="authType"
@close="closeAuthModal"
@success="onAuthSuccess"
@skip="onAuthSkip"
/>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, defineAsyncComponent } from 'vue'
import Taro from '@tarojs/taro'
// AuthModal 组件通过相对路径导入
const AuthModal = defineAsyncComponent(() => import('../../components/AuthModal/index.vue'))
import './index.scss'
// 静态资源
const logoSrc = 'https://wx-static.drip.im/img/limo/miniapp/logo.svg'
const homeIcon = require('@/assets/images/home.svg')
const userIcon = require('@/assets/images/user.svg')
const backIcon = require('@/assets/images/back.svg')
const roninLogoSrc = 'https://qiniu.drip.im/gh_09bd6126eab8/20250711/upload/4d0efe5313d38f0db01d0cfba4dac9d848bb43a2'
// 响应式状态
const selectedSport = ref('all')
const currentPage = ref('home') // 'home' | 'profile'
// 地理位置状态
const currentLocation = ref({
name: '定位中...',
address: '正在获取位置信息',
latitude: 0,
longitude: 0
})
const locationLoading = ref(false)
const locationReady = ref(false) // 位置信息是否已准备好
const locationError = ref(false) // 定位是否失败
// 授权弹窗状态
const authModalVisible = ref(false)
const authType = ref<'login' | 'video'>('login')
const isUserAuthorized = ref(false) // 用户是否已授权
// 方法
const selectSport = (sport: string) => {
selectedSport.value = sport
console.log('选择运动项目:', sport)
}
// 位置存储相关方法
const saveLocationToStorage = (locationData: any) => {
try {
Taro.setStorageSync('userLocation', {
...locationData,
timestamp: Date.now() // 记录保存时间
})
console.log('位置信息已保存到本地存储:', locationData)
} catch (error) {
console.error('保存位置信息失败:', error)
}
}
const loadLocationFromStorage = () => {
try {
const savedLocation = Taro.getStorageSync('userLocation')
if (savedLocation && savedLocation.address) {
// 检查位置信息是否过期7天有效期
const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000
const now = Date.now()
if (savedLocation.timestamp && (now - savedLocation.timestamp) < sevenDaysInMs) {
console.log('从本地存储加载位置信息:', savedLocation)
return savedLocation
} else {
console.log('位置信息已过期,清除本地存储')
clearLocationFromStorage()
}
}
} catch (error) {
console.error('读取位置信息失败:', error)
}
return null
}
const clearLocationFromStorage = () => {
try {
Taro.removeStorageSync('userLocation')
console.log('已清除本地存储的位置信息')
} catch (error) {
console.error('清除位置信息失败:', error)
}
}
const switchLocation = () => {
console.log('🔥 switchLocation 方法被调用!')
// 直接在地图上选择位置
chooseLocationOnMap()
}
// 在地图上选择位置
const chooseLocationOnMap = () => {
Taro.chooseLocation({
success: (res) => {
console.log('选择位置成功:', res)
// 更新当前位置信息
const locationData = {
name: res.name || res.address,
address: res.address,
latitude: res.latitude,
longitude: res.longitude
}
currentLocation.value = locationData
// 保存位置信息到本地存储
saveLocationToStorage(locationData)
locationReady.value = true
locationError.value = false
Taro.showToast({
title: '位置已更新',
icon: 'success'
})
},
fail: (err) => {
console.error('选择位置失败:', err)
// 回到选择位置状态
locationReady.value = false
locationError.value = true
currentLocation.value = {
name: '未选择位置',
address: '请选择您的位置以查看附近场馆',
latitude: 0,
longitude: 0
}
if (err.errMsg.includes('auth')) {
Taro.showModal({
title: '授权提示',
content: '需要授权位置信息才能选择地点,是否前往设置?',
success: (modalRes) => {
if (modalRes.confirm) {
Taro.openSetting()
}
}
})
} else if (!err.errMsg.includes('cancel')) {
// 只有在非取消的情况下才显示错误提示
Taro.showToast({
title: '选择位置失败',
icon: 'none'
})
}
}
})
}
// 获取当前位置
const getCurrentLocation = (showToast = true, showLoading = false) => {
if (!locationLoading.value) {
locationLoading.value = true
}
locationReady.value = false
locationError.value = false
if (showLoading) {
Taro.showLoading({
title: '定位中...'
})
}
Taro.getLocation({
type: 'gcj02', // 返回可以用于微信小程序的坐标
success: (res) => {
console.log('获取位置成功:', res)
// 简化处理直接显示坐标信息实际项目中可以调用逆地理编码API
const formattedAddress = `当前位置 (${res.latitude.toFixed(4)}, ${res.longitude.toFixed(4)})`
const locationData = {
name: '当前位置',
address: formattedAddress,
latitude: res.latitude,
longitude: res.longitude
}
currentLocation.value = locationData
// 保存位置信息到本地存储
saveLocationToStorage(locationData)
locationLoading.value = false
locationReady.value = true
locationError.value = false
if (showLoading) {
Taro.hideLoading()
}
if (showToast) {
Taro.showToast({
title: '定位成功',
icon: 'success'
})
}
},
fail: (err) => {
locationLoading.value = false
locationReady.value = false
locationError.value = true
if (showLoading) {
Taro.hideLoading()
}
console.error('获取位置失败:', err)
// 设置默认位置
currentLocation.value = {
name: '定位失败',
address: '无法获取位置信息,点击重试',
latitude: 0,
longitude: 0
}
if (err.errMsg.includes('auth')) {
Taro.showModal({
title: '授权提示',
content: '需要授权位置信息才能获取当前位置,是否前往设置?',
success: (modalRes) => {
if (modalRes.confirm) {
Taro.openSetting()
}
}
})
} else {
if (showToast) {
Taro.showToast({
title: '定位失败',
icon: 'none'
})
}
}
}
})
}
// 选择收货地址
const chooseDeliveryAddress = () => {
Taro.chooseAddress({
success: (res) => {
console.log('选择地址成功:', res)
const fullAddress = `${res.provinceName}${res.cityName}${res.countyName}${res.detailInfo}`
const locationData = {
name: res.userName,
address: fullAddress,
latitude: 0, // 收货地址API不返回坐标
longitude: 0
}
currentLocation.value = locationData
// 保存位置信息到本地存储
saveLocationToStorage(locationData)
locationReady.value = true
locationError.value = false
Taro.showToast({
title: '地址已更新',
icon: 'success'
})
},
fail: (err) => {
console.error('选择地址失败:', err)
if (err.errMsg.includes('auth')) {
Taro.showModal({
title: '授权提示',
content: '需要授权地址信息才能选择收货地址,是否前往设置?',
success: (modalRes) => {
if (modalRes.confirm) {
Taro.openSetting()
}
}
})
} else {
Taro.showToast({
title: '选择地址失败',
icon: 'none'
})
}
}
})
}
const playVideo = () => {
if (checkAuthRequired('video')) {
Taro.showToast({
title: '播放视频',
icon: 'none'
})
}
}
const enterZone = (venueType: string) => {
Taro.navigateTo({
url: '/pages/zone/index?venue=' + venueType
}).then(() => {
console.log('✅ 成功跳转到zone页面')
}).catch((err) => {
console.error('❌ 跳转失败:', err)
Taro.showToast({
title: '跳转失败',
icon: 'none'
})
})
}
// 页面切换方法
const switchToHome = () => {
currentPage.value = 'home'
}
const switchToProfile = () => {
currentPage.value = 'profile'
}
// 导航方法
const goHome = () => {
currentPage.value = 'home'
}
const goProfile = () => {
if (checkAuthRequired('login')) {
currentPage.value = 'profile'
}
}
// 我的页面功能方法
const editProfile = () => {
Taro.showToast({
title: '编辑用户信息功能开发中',
icon: 'none'
})
}
const openMemberBenefits = () => {
Taro.navigateTo({
url: '/pages/vip/index'
})
}
const goToVip = () => {
Taro.navigateTo({
url: '/pages/vip/index'
})
}
const goToFavorites = () => {
Taro.navigateTo({
url: '/pages/favorites/index'
})
}
const goToHistory = () => {
Taro.navigateTo({
url: '/pages/history/index'
})
}
const goToAddress = () => {
Taro.showToast({
title: '常用地址',
icon: 'none'
})
}
const goToNotifications = () => {
Taro.navigateTo({
url: '/pages/notifications/index'
})
}
const goToOrders = () => {
Taro.navigateTo({
url: '/pages/orders/index'
})
}
const goToActivityHistory = () => {
Taro.navigateTo({
url: '/pages/activity/index'
})
}
const goToPrivacy = () => {
Taro.showToast({
title: '隐私设置',
icon: 'none'
})
}
// 授权相关方法
const showAuthModal = (type: 'login' | 'video') => {
authType.value = type
authModalVisible.value = true
}
const closeAuthModal = () => {
authModalVisible.value = false
}
const onAuthSuccess = (data: { phone?: string, userInfo?: any }) => {
console.log('授权成功:', data)
isUserAuthorized.value = true
// 更新用户信息到本地存储
if (data.phone) {
Taro.setStorageSync('userPhone', data.phone)
}
if (data.userInfo) {
Taro.setStorageSync('userInfo', data.userInfo)
}
Taro.showToast({
title: '授权成功',
icon: 'success'
})
}
const onAuthSkip = () => {
console.log('用户跳过授权')
// 可以记录用户跳过授权的行为
}
// 检查是否需要授权
const checkAuthRequired = (action: 'login' | 'video') => {
// 检查本地是否有授权信息
const hasPhone = Taro.getStorageSync('userPhone')
const hasUserInfo = Taro.getStorageSync('userInfo')
if (!hasPhone || !hasUserInfo) {
showAuthModal(action)
return false
}
isUserAuthorized.value = true
return true
}
// 注意: playVideo 和 goProfile 方法在下面已有定义,这里只是添加了授权检查逻辑
onMounted(() => {
console.log('LIMO来刻首页已加载')
console.log('Vue组件已挂载方法可用')
// 检查用户授权状态
const hasPhone = Taro.getStorageSync('userPhone')
const hasUserInfo = Taro.getStorageSync('userInfo')
if (hasPhone && hasUserInfo) {
isUserAuthorized.value = true
}
// 尝试从本地存储加载位置信息
const savedLocation = loadLocationFromStorage()
if (savedLocation) {
// 如果有保存的位置信息,直接使用
currentLocation.value = {
name: savedLocation.name,
address: savedLocation.address,
latitude: savedLocation.latitude,
longitude: savedLocation.longitude
}
locationLoading.value = false
locationReady.value = true
locationError.value = false
console.log('使用已保存的位置信息')
} else {
// 如果没有保存的位置信息,显示选择位置引导
locationLoading.value = false
locationReady.value = false
locationError.value = true
currentLocation.value = {
name: '未选择位置',
address: '请选择您的位置以查看附近场馆',
latitude: 0,
longitude: 0
}
console.log('未找到保存的位置信息,显示选择引导')
}
})
</script>