This commit is contained in:
yison 2025-07-11 18:28:43 +08:00
parent dc24d5b6ed
commit cd11b355bb
25 changed files with 3632 additions and 186 deletions

View file

@ -2,7 +2,12 @@ export default defineAppConfig({
pages: [
'pages/index/index',
'pages/zone/index',
'pages/vip/index'
'pages/vip/index',
'pages/favorites/index',
'pages/history/index',
'pages/notifications/index',
'pages/orders/index',
'pages/activity/index'
],
window: {
backgroundTextStyle: 'light',
@ -10,5 +15,17 @@ export default defineAppConfig({
navigationBarTitleText: 'LIMO来刻',
navigationBarTextStyle: 'white',
navigationStyle: 'custom'
},
// 声明需要的隐私接口
requiredPrivateInfos: [
'getLocation',
'chooseLocation',
'chooseAddress'
],
// 位置权限说明
permission: {
'scope.userLocation': {
desc: '您的位置信息将用于推荐附近的运动场馆'
}
}
})

View file

@ -0,0 +1,580 @@
.auth-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
animation: fadeIn 0.3s ease-out;
}
.modal-content {
position: relative;
z-index: 10;
background: #ffffff;
border-radius: 32rpx;
padding: 48rpx 32rpx;
width: 100%;
max-width: 640rpx;
animation: slideUp 0.3s ease-out;
box-shadow: 0 32rpx 96rpx rgba(0, 0, 0, 0.2);
.modal-header {
text-align: center;
margin-bottom: 32rpx;
.logo-container {
margin-bottom: 24rpx;
.app-logo {
width: 120rpx;
height: 120rpx;
}
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #111827;
display: block;
margin-bottom: 12rpx;
}
.modal-subtitle {
font-size: 26rpx;
color: #6B7280;
display: block;
}
}
// 步骤指示器
.step-indicator {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 48rpx;
padding: 0 32rpx;
.step-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.step-number {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: #F3F4F6;
color: #9CA3AF;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: 600;
margin-bottom: 12rpx;
border: 3rpx solid #E5E7EB;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.step-text {
font-size: 20rpx;
color: #9CA3AF;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
&.active {
.step-number {
background: linear-gradient(135deg, #05C7C7 0%, #04B5B5 100%);
color: #ffffff;
border-color: #05C7C7;
box-shadow: 0 8rpx 24rpx rgba(5, 199, 199, 0.3);
}
.step-text {
color: #05C7C7;
font-weight: 600;
}
}
&.completed {
.step-number {
background: #10B981;
color: #ffffff;
border-color: #10B981;
}
.step-text {
color: #10B981;
}
}
}
.step-line {
width: 80rpx;
height: 4rpx;
background: #E5E7EB;
margin: 0 24rpx;
margin-bottom: 32rpx;
border-radius: 2rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.active {
background: linear-gradient(135deg, #05C7C7 0%, #04B5B5 100%);
box-shadow: 0 2rpx 8rpx rgba(5, 199, 199, 0.3);
}
}
}
// 授权步骤容器
.auth-step {
animation: fadeInUp 0.3s ease-out;
}
.auth-info {
margin-bottom: 48rpx;
.info-item {
display: flex;
align-items: center;
padding: 24rpx;
background: #F9FAFB;
border-radius: 24rpx;
margin-bottom: 24rpx;
border: 2rpx solid #F3F4F6;
&:last-child {
margin-bottom: 0;
}
.info-icon {
width: 80rpx;
height: 80rpx;
background: #ffffff;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.icon-text {
font-size: 36rpx;
}
}
.info-content {
flex: 1;
.info-title {
font-size: 28rpx;
font-weight: 600;
color: #111827;
display: block;
margin-bottom: 0;
}
.info-desc {
font-size: 24rpx;
color: #6B7280;
line-height: 1;
}
}
}
}
.auth-buttons {
margin-bottom: 32rpx;
.auth-btn {
width: 100%;
height: 96rpx;
border-radius: 24rpx;
border: none;
font-size: 28rpx;
font-weight: 600;
margin-bottom: 24rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
&:last-child {
margin-bottom: 0;
}
&:active {
transform: scale(0.98);
}
&.primary {
background: linear-gradient(135deg, #05C7C7 0%, #04B5B5 100%);
color: #ffffff;
&:disabled {
background: #10B981;
opacity: 0.8;
}
}
&.secondary {
background: #F3F4F6;
color: #374151;
border: 2rpx solid #E5E7EB;
&:disabled {
background: #10B981;
color: #ffffff;
border-color: #10B981;
}
}
.btn-text {
position: relative;
z-index: 2;
line-height: 1;
}
// 按钮光泽效果
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
&:active::before {
left: 100%;
}
}
}
// 昵称输入区域
.nickname-input-section {
margin-bottom: 32rpx;
.input-label {
margin-bottom: 16rpx;
.label-text {
font-size: 28rpx;
font-weight: 600;
color: #111827;
}
}
.nickname-input {
width: 100%;
height: 96rpx;
background: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 24rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #111827;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:focus {
border-color: #05C7C7;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(5, 199, 199, 0.1);
}
&::placeholder {
color: #9CA3AF;
font-size: 26rpx;
}
}
}
.auth-actions {
margin-bottom: 32rpx;
.complete-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #FBBF24 0%, #F59E0B 100%);
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 16rpx 48rpx rgba(251, 191, 36, 0.3);
&:active {
transform: scale(0.98);
}
.complete-text {
font-size: 32rpx;
font-weight: bold;
color: #ffffff;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
}
.next-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #05C7C7 0%, #04B5B5 100%);
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 16rpx 48rpx rgba(5, 199, 199, 0.3);
&:active {
transform: scale(0.98);
}
.next-text {
font-size: 32rpx;
font-weight: bold;
color: #ffffff;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
}
.action-row {
display: flex;
gap: 24rpx;
align-items: center;
.back-btn {
flex: 1;
height: 80rpx;
background: #F3F4F6;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2rpx solid #E5E7EB;
&:active {
transform: scale(0.98);
background: #E5E7EB;
}
.back-text {
font-size: 28rpx;
font-weight: 600;
color: #6B7280;
}
}
.skip-btn {
flex: 1;
text-align: center;
padding: 24rpx;
.skip-text {
font-size: 28rpx;
color: #9CA3AF;
text-decoration: underline;
}
}
}
.skip-btn {
text-align: center;
padding: 24rpx;
.skip-text {
font-size: 28rpx;
color: #9CA3AF;
text-decoration: underline;
}
}
}
.privacy-notice {
text-align: center;
padding: 0 16rpx;
.notice-text {
font-size: 24rpx;
color: #9CA3AF;
line-height: 1.5;
.link-text {
color: #05C7C7;
text-decoration: underline;
}
}
}
}
}
// 动画
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(100rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// 响应式设计
@media (max-width: 480px) {
.auth-modal {
padding: 24rpx;
.modal-content {
padding: 40rpx 24rpx;
.modal-header {
margin-bottom: 24rpx;
.logo-container .app-logo {
width: 100rpx;
height: 100rpx;
}
.modal-title {
font-size: 32rpx;
}
.modal-subtitle {
font-size: 24rpx;
}
}
.step-indicator {
margin-bottom: 40rpx;
padding: 0 16rpx;
.step-item {
.step-number {
width: 56rpx;
height: 56rpx;
font-size: 22rpx;
margin-bottom: 8rpx;
}
.step-text {
font-size: 18rpx;
}
}
.step-line {
width: 60rpx;
margin: 0 16rpx;
margin-bottom: 24rpx;
}
}
.auth-info {
margin-bottom: 40rpx;
.info-item {
padding: 20rpx;
.info-icon {
width: 72rpx;
height: 72rpx;
margin-right: 20rpx;
.icon-text {
font-size: 32rpx;
}
}
.info-content {
.info-title {
font-size: 26rpx;
}
.info-desc {
font-size: 22rpx;
}
}
}
}
.auth-buttons .auth-btn {
height: 88rpx;
font-size: 26rpx;
}
.nickname-input-section {
margin-bottom: 24rpx;
.input-label .label-text {
font-size: 26rpx;
}
.nickname-input {
height: 80rpx;
font-size: 26rpx;
padding: 0 20rpx;
&::placeholder {
font-size: 24rpx;
}
}
}
.auth-actions {
.complete-btn, .next-btn {
height: 80rpx;
.complete-text, .next-text {
font-size: 28rpx;
}
}
.action-row .back-btn {
height: 72rpx;
.back-text {
font-size: 26rpx;
}
}
}
}
}
}

View file

@ -0,0 +1,299 @@
<template>
<view v-if="visible" class="auth-modal">
<view class="modal-overlay" @tap="closeModal"></view>
<view class="modal-content">
<view class="modal-header">
<view class="logo-container">
<image :src="logoSrc" class="app-logo" mode="aspectFit" />
</view>
<text class="modal-title">欢迎使用LIMO来刻</text>
<text class="modal-subtitle">{{ getSubtitle() }}</text>
</view>
<!-- 步骤指示器 -->
<view class="step-indicator">
<view class="step-item" :class="{ active: currentStep >= 1, completed: currentStep > 1 }">
<view class="step-number">1</view>
<text class="step-text">手机号</text>
</view>
<view class="step-line" :class="{ active: currentStep > 1 }"></view>
<view class="step-item" :class="{ active: currentStep >= 2, completed: currentStep > 2 }">
<view class="step-number">2</view>
<text class="step-text">个人信息</text>
</view>
</view>
<!-- 第一步手机号授权 -->
<view v-if="currentStep === 1" class="auth-step">
<view class="auth-info">
<view class="info-item">
<view class="info-icon">
<text class="icon-text">📱</text>
</view>
<view class="info-content">
<text class="info-title">手机号授权</text>
<text class="info-desc">用于账号安全登录验证和消息通知</text>
</view>
</view>
</view>
<view class="auth-buttons">
<button
class="auth-btn primary"
open-type="getPhoneNumber"
@getphonenumber="onGetPhoneNumber"
:disabled="phoneAuthorized"
>
<text class="btn-text">
{{ phoneAuthorized ? '✓ 手机号已授权' : '授权手机号' }}
</text>
</button>
</view>
<view class="auth-actions">
<view v-if="phoneAuthorized" class="next-btn" @tap="goToNextStep">
<text class="next-text">下一步</text>
</view>
<view v-else class="skip-btn" @tap="skipCurrentStep">
<text class="skip-text">暂不授权</text>
</view>
</view>
</view>
<!-- 第二步昵称头像授权 -->
<view v-if="currentStep === 2" class="auth-step">
<view class="auth-info">
<view class="info-item">
<view class="info-icon">
<text class="icon-text">👤</text>
</view>
<view class="info-content">
<text class="info-title">设置个人信息</text>
<text class="info-desc">请选择头像并输入昵称用于个性化展示</text>
</view>
</view>
</view>
<view class="auth-buttons">
<button
class="auth-btn primary"
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
:disabled="profileAuthorized"
>
<text class="btn-text">
{{ profileAuthorized ? '✓ 头像已选择' : '选择头像' }}
</text>
</button>
</view>
<!-- 昵称输入框 -->
<view v-if="!profileAuthorized" class="nickname-input-section">
<view class="input-label">
<text class="label-text">请输入昵称</text>
</view>
<input
class="nickname-input"
type="nickname"
placeholder="请输入您的昵称"
:value="userNickname"
@input="onNicknameInput"
:maxlength="20"
/>
</view>
<view class="auth-actions">
<view v-if="profileAuthorized" class="complete-btn" @tap="completeAuth">
<text class="complete-text">完成设置</text>
</view>
<view v-else class="action-row">
<view class="back-btn" @tap="goToPrevStep">
<text class="back-text">上一步</text>
</view>
<view class="skip-btn" @tap="skipCurrentStep">
<text class="skip-text">暂不授权</text>
</view>
</view>
</view>
</view>
<view class="privacy-notice">
<text class="notice-text">
我们承诺保护您的隐私安全详见
<text class="link-text" @tap="showPrivacyPolicy">隐私政策</text>
</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, defineEmits, defineProps } from 'vue'
import Taro from '@tarojs/taro'
import './index.scss'
// Props
interface Props {
visible: boolean
authType?: 'login' | 'video' //
}
const props = withDefaults(defineProps<Props>(), {
authType: 'login'
})
// Emits
const emit = defineEmits<{
close: []
success: [data: { phone?: string, userInfo?: any }]
skip: []
}>()
//
const currentStep = ref(1) // 1=2=
const phoneAuthorized = ref(false)
const profileAuthorized = ref(false)
const userNickname = ref('')
const userAvatar = ref('')
const userAuthData = ref<{
phone?: string
userInfo?: any
}>({})
//
const logoSrc = 'https://wx-static.drip.im/img/limo/miniapp/logo.svg'
//
const closeModal = () => {
//
currentStep.value = 1
phoneAuthorized.value = false
profileAuthorized.value = false
userNickname.value = ''
userAvatar.value = ''
userAuthData.value = {}
emit('close')
}
//
const getSubtitle = () => {
if (currentStep.value === 1) {
return '第一步:验证手机号,确保账号安全'
} else {
return '第二步:完善个人信息,获得更好体验'
}
}
//
const goToNextStep = () => {
if (currentStep.value < 2) {
currentStep.value++
}
}
const goToPrevStep = () => {
if (currentStep.value > 1) {
currentStep.value--
}
}
const skipCurrentStep = () => {
//
emit('skip')
closeModal()
}
const onGetPhoneNumber = (e: any) => {
console.log('获取手机号:', e.detail)
if (e.detail.errMsg === 'getPhoneNumber:ok') {
//
//
phoneAuthorized.value = true
userAuthData.value.phone = '138****8888' //
Taro.showToast({
title: '手机号授权成功',
icon: 'success',
duration: 1500
})
// 1.5
setTimeout(() => {
goToNextStep()
}, 1500)
} else {
Taro.showToast({
title: '手机号授权失败',
icon: 'none',
duration: 2000
})
}
}
const onChooseAvatar = (e: any) => {
console.log('选择头像:', e.detail)
if (e.detail.avatarUrl) {
userAvatar.value = e.detail.avatarUrl
//
checkProfileComplete()
Taro.showToast({
title: '头像选择成功',
icon: 'success',
duration: 1000
})
} else {
Taro.showToast({
title: '头像选择失败',
icon: 'none',
duration: 2000
})
}
}
const onNicknameInput = (e: any) => {
userNickname.value = e.detail.value
//
checkProfileComplete()
}
const checkProfileComplete = () => {
if (userAvatar.value && userNickname.value.trim()) {
profileAuthorized.value = true
userAuthData.value.userInfo = {
avatarUrl: userAvatar.value,
nickName: userNickname.value.trim()
}
Taro.showToast({
title: '个人信息完善成功',
icon: 'success',
duration: 1500
})
// 1.5
setTimeout(() => {
completeAuth()
}, 1500)
}
}
const completeAuth = () => {
emit('success', userAuthData.value)
closeModal()
}
const showPrivacyPolicy = () => {
Taro.showModal({
title: '隐私政策',
content: '我们严格按照相关法律法规保护您的个人信息安全,不会泄露给第三方。',
showCancel: false
})
}
</script>

View file

@ -2,3 +2,4 @@
*
*/
export { default as CommonButton } from './CommonButton/index.vue'
export { default as AuthModal } from './AuthModal/index.vue'

View file

@ -0,0 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '历史活动',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
navigationStyle: 'default'
})

View file

@ -0,0 +1,137 @@
.activity-page {
min-height: 100vh;
background: #f9fafb;
// Empty State
.empty-state {
padding: 120rpx 48rpx;
display: flex;
justify-content: center;
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
.empty-icon {
font-size: 120rpx;
margin-bottom: 32rpx;
opacity: 0.6;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: #374151;
margin-bottom: 16rpx;
}
.empty-subtitle {
font-size: 26rpx;
color: #9ca3af;
line-height: 1.5;
}
}
}
// Activity List
.activity-list {
padding: 24rpx;
.date-group {
margin-bottom: 48rpx;
&:last-child {
margin-bottom: 24rpx;
}
.date-header {
margin-bottom: 24rpx;
padding: 0 8rpx;
.date-text {
font-size: 28rpx;
font-weight: 600;
color: #374151;
position: relative;
&::after {
content: '';
position: absolute;
bottom: -8rpx;
left: 0;
width: 48rpx;
height: 4rpx;
background: linear-gradient(135deg, #05c7c7 0%, #04b5b5 100%);
border-radius: 2rpx;
}
}
}
.activities {
.activity-item {
background: #ffffff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 16rpx;
border: 2rpx solid #f3f4f6;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:active {
transform: scale(0.995);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
&:last-child {
margin-bottom: 0;
}
.activity-content {
width: 100%;
.activity-title {
font-size: 28rpx;
font-weight: 600;
color: #111827;
margin-bottom: 8rpx;
line-height: 1.4;
display: block;
}
.activity-description {
font-size: 24rpx;
color: #6b7280;
margin-bottom: 12rpx;
line-height: 1.4;
display: block;
}
.activity-time {
font-size: 22rpx;
color: #9ca3af;
display: block;
}
}
}
}
}
}
// Load More
.load-more {
padding: 32rpx 24rpx;
text-align: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:active {
transform: scale(0.98);
}
.load-more-text {
font-size: 26rpx;
color: #6b7280;
font-weight: 500;
}
}
}

View file

@ -0,0 +1,202 @@
<template>
<view class="activity-page">
<!-- Empty State -->
<view v-if="activities.length === 0" class="empty-state">
<view class="empty-container">
<text class="empty-icon">📝</text>
<text class="empty-title">暂无活动记录</text>
<text class="empty-subtitle">开始使用LIMO来刻记录您的运动时光</text>
</view>
</view>
<!-- Activity List -->
<view v-else class="activity-list">
<!-- Date Group -->
<view
v-for="group in groupedActivities"
:key="group.date"
class="date-group"
>
<view class="date-header">
<text class="date-text">{{ group.date }}</text>
</view>
<view class="activities">
<view
v-for="activity in group.activities"
:key="activity.id"
class="activity-item"
@tap="handleActivityClick(activity)"
>
<view class="activity-content">
<text class="activity-title">{{ activity.title }}</text>
<text class="activity-description">{{ activity.description }}</text>
<text class="activity-time">{{ activity.time }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- Load More -->
<view v-if="hasMore" class="load-more" @tap="loadMoreActivities">
<text class="load-more-text">{{ loading ? '加载中...' : '加载更多' }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import './index.scss'
//
const loading = ref(false)
const hasMore = ref(true)
//
const activities = ref([
{
id: 'act001',
title: '观看了 RONIN黄金篮球馆',
description: '观看时长 45分钟 · 篮球训练课程',
time: '14:30',
date: '2025-01-12'
},
{
id: 'act002',
title: '收藏了 Panda惊怒熊猫运动俱乐部',
description: '已添加到我的收藏 · 综合运动',
time: '10:15',
date: '2025-01-12'
},
{
id: 'act003',
title: '获得成就「初来乍到」',
description: '完成首次场馆观看',
time: '09:45',
date: '2025-01-12'
},
{
id: 'act004',
title: '观看了 星网网球俱乐部',
description: '观看时长 28分钟 · 网球比赛回放',
time: '19:20',
date: '2025-01-11'
},
{
id: 'act005',
title: '点赞了 @张三 的精彩回放',
description: '篮球 · RONIN黄金篮球馆',
time: '18:45',
date: '2025-01-11'
},
{
id: 'act006',
title: '取消收藏 天河体育中心',
description: '已从收藏中移除 · 综合体育场',
time: '16:30',
date: '2025-01-11'
},
{
id: 'act007',
title: '观看了 金牌羽毛球馆',
description: '观看时长 35分钟 · 羽毛球教学',
time: '15:10',
date: '2025-01-11'
},
{
id: 'act008',
title: '评论了 RONIN黄金篮球馆',
description: '"场地很不错,设施完善!" · 收到 3 个赞',
time: '14:25',
date: '2025-01-10'
},
{
id: 'act009',
title: '获得成就「社交达人」',
description: '累计获得 10 个点赞',
time: '12:00',
date: '2025-01-10'
},
{
id: 'act010',
title: '观看了 蓝色港湾游泳馆',
description: '观看时长 52分钟 · 游泳技巧分享',
time: '08:30',
date: '2025-01-10'
}
])
//
const groupedActivities = computed(() => {
const grouped = new Map()
activities.value.forEach(activity => {
const dateKey = activity.date
if (!grouped.has(dateKey)) {
grouped.set(dateKey, {
date: formatDate(activity.date),
activities: []
})
}
grouped.get(dateKey).activities.push(activity)
})
return Array.from(grouped.values()).sort((a, b) => {
//
return new Date(b.date).getTime() - new Date(a.date).getTime()
})
})
//
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)
const today = new Date()
const yesterday = new Date(today)
yesterday.setDate(today.getDate() - 1)
if (dateStr === today.toISOString().split('T')[0]) {
return '今天'
} else if (dateStr === yesterday.toISOString().split('T')[0]) {
return '昨天'
} else {
return date.toLocaleDateString('zh-CN', {
month: 'long',
day: 'numeric',
weekday: 'short'
})
}
}
const handleActivityClick = (activity: any) => {
Taro.showModal({
title: '活动详情',
content: `${activity.title}\n${activity.description}`,
showCancel: false
})
}
const loadMoreActivities = () => {
if (loading.value) return
loading.value = true
//
setTimeout(() => {
loading.value = false
//
// hasMore.value = false
Taro.showToast({
title: '暂无更多数据',
icon: 'none'
})
hasMore.value = false
}, 1000)
}
onMounted(() => {
console.log('历史活动页面已加载')
})
</script>

View file

@ -0,0 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '我的收藏',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
navigationStyle: 'default'
})

View file

@ -0,0 +1,166 @@
.favorites-page {
min-height: 100vh;
background: #f8fafc;
padding-bottom: 32rpx;
}
.page-header {
background: #ffffff;
padding: 48rpx 32rpx 32rpx 32rpx;
border-bottom: 2rpx solid #f1f5f9;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
color: #1e293b;
display: block;
margin-bottom: 8rpx;
}
.header-subtitle {
font-size: 26rpx;
color: #64748b;
display: block;
}
.empty-state {
padding: 120rpx 32rpx;
min-height: 600rpx;
display: flex;
align-items: center;
justify-content: center;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 24rpx;
}
.empty-icon {
font-size: 96rpx;
margin-bottom: 16rpx;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: #374151;
}
.empty-subtitle {
font-size: 26rpx;
color: #6b7280;
line-height: 1.5;
max-width: 400rpx;
}
.empty-button {
background: linear-gradient(135deg, #05c7c7 0%, #04b5b5 100%);
color: #ffffff;
padding: 24rpx 48rpx;
border-radius: 32rpx;
margin-top: 16rpx;
box-shadow: 0 8rpx 24rpx rgba(5, 199, 199, 0.2);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.empty-button:active {
transform: scale(0.95);
}
.favorites-list {
padding: 16rpx;
}
.venue-card {
background: #ffffff;
border-radius: 16rpx;
margin-bottom: 16rpx;
padding: 24rpx;
border: 2rpx solid #f1f5f9;
}
.venue-info {
width: 100%;
}
.venue-name {
font-size: 30rpx;
font-weight: bold;
color: #1e293b;
margin-bottom: 8rpx;
display: block;
}
.venue-address {
font-size: 24rpx;
color: #64748b;
line-height: 1.4;
margin-bottom: 16rpx;
display: block;
}
.venue-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.venue-distance {
font-size: 22rpx;
color: #05c7c7;
font-weight: 500;
}
.venue-actions {
display: flex;
gap: 12rpx;
}
.action-btn {
padding: 12rpx 20rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
transition: all 0.3s ease;
}
.action-btn:active {
transform: scale(0.95);
}
.remove-btn {
background: #f8f9fa;
color: #6c757d;
border: 2rpx solid #e9ecef;
}
.remove-btn:active {
background: #e9ecef;
}
.zone-btn {
background: linear-gradient(135deg, #05c7c7 0%, #04b5b5 100%);
color: #ffffff;
}
.zone-btn:active {
background: linear-gradient(135deg, #04b5b5 0%, #039a9a 100%);
}
.btn-text {
font-size: 22rpx;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}

View file

@ -0,0 +1,113 @@
<template>
<view class="favorites-page">
<!-- Header -->
<view class="page-header">
<text class="header-title">我的收藏</text>
<text class="header-subtitle"> {{ favoriteVenues.length }} 个收藏场馆</text>
</view>
<!-- Empty State -->
<view v-if="favoriteVenues.length === 0" class="empty-state">
<view class="empty-container">
<text class="empty-icon">💝</text>
<text class="empty-title">暂无收藏场馆</text>
<text class="empty-subtitle">快去发现喜欢的运动场馆吧</text>
<view class="empty-button" @tap="goToHome">
<text class="button-text">去发现</text>
</view>
</view>
</view>
<!-- Favorites List -->
<view v-else class="favorites-list">
<view
v-for="venue in favoriteVenues"
:key="venue.id"
class="venue-card"
>
<view class="venue-info">
<text class="venue-name">{{ venue.name }}</text>
<text class="venue-address">{{ venue.address }}</text>
<view class="venue-footer">
<text class="venue-distance">{{ venue.distance }}</text>
<view class="venue-actions">
<view class="action-btn remove-btn" @tap.stop="toggleFavorite(venue)">
<text class="btn-text">移除收藏</text>
</view>
<view class="action-btn zone-btn" @tap.stop="enterZone(venue)">
<text class="btn-text">进入ZONE</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import './index.scss'
//
const favoriteVenues = ref([
{
id: 'ronin',
name: 'RONIN黄金篮球馆',
address: '杭州拱墅区黑马路124号肯德基对面',
distance: '120m'
},
{
id: 'panda',
name: 'Panda惊怒熊猫运动俱乐部',
address: '杭州滨江区江南大道456号',
distance: '340m'
},
{
id: 'tennis',
name: '星网网球俱乐部',
address: '杭州西湖区文三路789号',
distance: '1.2km'
}
])
//
const goToHome = () => {
Taro.navigateBack()
}
const toggleFavorite = (venue: any) => {
Taro.showModal({
title: '取消收藏',
content: `确定要取消收藏"${venue.name}"吗?`,
success: (res) => {
if (res.confirm) {
//
const index = favoriteVenues.value.findIndex(v => v.id === venue.id)
if (index > -1) {
favoriteVenues.value.splice(index, 1)
Taro.showToast({
title: '已取消收藏',
icon: 'success'
})
}
}
}
})
}
const enterZone = (venue: any) => {
Taro.navigateTo({
url: `/pages/zone/index?venue=${venue.id}`
})
}
onMounted(() => {
console.log('我的收藏页面已加载')
})
</script>

View file

@ -0,0 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '观看历史',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
navigationStyle: 'default'
})

View file

@ -0,0 +1,233 @@
.history-page {
min-height: 100vh;
background: #f8fafc;
padding-bottom: 32rpx;
}
.page-header {
background: #ffffff;
padding: 48rpx 32rpx 32rpx 32rpx;
border-bottom: 2rpx solid #f1f5f9;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
color: #1e293b;
display: block;
margin-bottom: 8rpx;
}
.header-subtitle {
font-size: 26rpx;
color: #64748b;
display: block;
}
.empty-state {
padding: 120rpx 32rpx;
min-height: 600rpx;
display: flex;
align-items: center;
justify-content: center;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 24rpx;
}
.empty-icon {
font-size: 96rpx;
margin-bottom: 16rpx;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: #374151;
}
.empty-subtitle {
font-size: 26rpx;
color: #6b7280;
line-height: 1.5;
max-width: 400rpx;
}
.empty-button {
background: linear-gradient(135deg, #05c7c7 0%, #04b5b5 100%);
color: #ffffff;
padding: 24rpx 48rpx;
border-radius: 32rpx;
margin-top: 16rpx;
box-shadow: 0 8rpx 24rpx rgba(5, 199, 199, 0.2);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.empty-button:active {
transform: scale(0.95);
}
.history-list {
padding: 16rpx;
}
.video-card {
background: #ffffff;
border-radius: 16rpx;
margin-bottom: 16rpx;
padding: 16rpx;
border: 2rpx solid #f1f5f9;
display: flex;
gap: 16rpx;
transition: all 0.3s ease;
}
.video-card:active {
background: #f8fafc;
transform: translateY(-1rpx);
}
.video-thumbnail {
position: relative;
width: 200rpx;
height: 120rpx;
flex-shrink: 0;
}
.thumbnail-placeholder {
width: 100%;
height: 100%;
border-radius: 12rpx;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.ronin-bg {
background: linear-gradient(135deg, #3b82f6 0%, #dc2626 100%);
}
.panda-bg {
background: linear-gradient(135deg, #8b5cf6 0%, #06b6d4 100%);
}
.tennis-bg {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
}
.play-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48rpx;
height: 48rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.play-icon {
color: #ffffff;
font-size: 20rpx;
margin-left: 4rpx;
}
.video-duration {
position: absolute;
bottom: 8rpx;
right: 8rpx;
background: rgba(0, 0, 0, 0.8);
color: #ffffff;
padding: 4rpx 8rpx;
border-radius: 6rpx;
font-size: 18rpx;
}
.video-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 8rpx;
}
.venue-name {
font-size: 28rpx;
font-weight: 600;
color: #1e293b;
line-height: 1.3;
}
.video-meta {
display: flex;
align-items: center;
gap: 16rpx;
}
.watch-time {
font-size: 22rpx;
color: #64748b;
}
.video-duration-text {
font-size: 22rpx;
color: #9ca3af;
}
.video-actions {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s ease;
}
.video-actions:active {
background: #f3f4f6;
}
.more-icon {
font-size: 32rpx;
color: #9ca3af;
transform: rotate(90deg);
}
.clear-section {
padding: 48rpx 32rpx;
display: flex;
justify-content: center;
}
.clear-button {
background: #f8f9fa;
color: #dc3545;
padding: 20rpx 40rpx;
border-radius: 24rpx;
border: 2rpx solid #dc3545;
transition: all 0.3s ease;
}
.clear-button:active {
background: #dc3545;
color: #ffffff;
transform: scale(0.95);
}
.clear-text {
font-size: 26rpx;
font-weight: 500;
}

View file

@ -0,0 +1,165 @@
<template>
<view class="history-page">
<!-- Header -->
<view class="page-header">
<text class="header-title">观看历史</text>
<text class="header-subtitle">共观看 {{ watchHistory.length }} 个视频</text>
</view>
<!-- Empty State -->
<view v-if="watchHistory.length === 0" class="empty-state">
<view class="empty-container">
<text class="empty-icon">📺</text>
<text class="empty-title">暂无观看记录</text>
<text class="empty-subtitle">去发现精彩的运动视频吧</text>
<view class="empty-button" @tap="goToHome">
<text class="button-text">去发现</text>
</view>
</view>
</view>
<!-- History List -->
<view v-else class="history-list">
<view
v-for="record in watchHistory"
:key="record.id"
class="video-card"
@tap="playVideo(record)"
>
<!-- Video Thumbnail -->
<view class="video-thumbnail">
<view class="thumbnail-placeholder" :class="record.thumbnailClass">
<view class="play-overlay">
<text class="play-icon"></text>
</view>
</view>
<view class="video-duration">{{ record.videoDuration }}</view>
</view>
<!-- Video Info -->
<view class="video-info">
<text class="venue-name">{{ record.venueName }}</text>
<view class="video-meta">
<text class="watch-time">{{ record.watchTime }}</text>
<text class="video-duration-text">{{ record.videoDuration }}</text>
</view>
</view>
<!-- More Options -->
<view class="video-actions" @tap.stop="showVideoOptions(record)">
<text class="more-icon"></text>
</view>
</view>
</view>
<!-- Clear History Button -->
<view v-if="watchHistory.length > 0" class="clear-section">
<view class="clear-button" @tap="clearHistory">
<text class="clear-text">清空观看历史</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import './index.scss'
//
const watchHistory = ref([
{
id: '1',
venueName: 'RONIN黄金篮球馆',
videoDuration: '12:45',
watchTime: '2小时前',
thumbnailClass: 'ronin-bg',
venueId: 'ronin'
},
{
id: '2',
venueName: 'Panda惊怒熊猫运动俱乐部',
videoDuration: '08:32',
watchTime: '1天前',
thumbnailClass: 'panda-bg',
venueId: 'panda'
},
{
id: '3',
venueName: '星网网球俱乐部',
videoDuration: '15:28',
watchTime: '2天前',
thumbnailClass: 'tennis-bg',
venueId: 'tennis'
},
{
id: '4',
venueName: 'RONIN黄金篮球馆',
videoDuration: '25:51',
watchTime: '3天前',
thumbnailClass: 'ronin-bg',
venueId: 'ronin'
},
{
id: '5',
venueName: 'Panda惊怒熊猫运动俱乐部',
videoDuration: '06:15',
watchTime: '5天前',
thumbnailClass: 'panda-bg',
venueId: 'panda'
}
])
//
const goToHome = () => {
Taro.navigateBack()
}
const playVideo = (record: any) => {
console.log('播放视频:', record.venueName)
Taro.showToast({
title: '播放视频中...',
icon: 'none'
})
// TODO:
}
const showVideoOptions = (record: any) => {
Taro.showActionSheet({
itemList: ['删除观看记录'],
success: (res) => {
if (res.tapIndex === 0) {
//
const index = watchHistory.value.findIndex(v => v.id === record.id)
if (index > -1) {
watchHistory.value.splice(index, 1)
Taro.showToast({
title: '已删除记录',
icon: 'success'
})
}
}
}
})
}
const clearHistory = () => {
Taro.showModal({
title: '清空观看历史',
content: '确定要清空所有视频观看记录吗?此操作不可恢复。',
success: (res) => {
if (res.confirm) {
watchHistory.value = []
Taro.showToast({
title: '已清空观看历史',
icon: 'success'
})
}
}
})
}
onMounted(() => {
console.log('观看历史页面已加载')
})
</script>

View file

@ -1,3 +1,22 @@
// 全局动画
@keyframes pulse {
0%, 100% {
opacity: 0.8;
}
50% {
opacity: 0.4;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.limo-mobile {
min-height: 100vh;
background: #ffffff;
@ -237,6 +256,12 @@
flex: 1;
color: #6B7280;
font-size: 24rpx;
&.location-loading {
color: #9CA3AF;
opacity: 0.8;
animation: pulse 1.5s infinite;
}
}
.location-switch {
@ -247,6 +272,69 @@
}
}
// 位置状态提示页面
.location-status-section {
padding: 120rpx 32rpx;
min-height: 600rpx;
display: flex;
align-items: center;
justify-content: center;
.status-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 32rpx;
.loading-spinner {
width: 80rpx;
height: 80rpx;
border: 6rpx solid #f3f3f3;
border-top: 6rpx solid #05C7C7;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.status-icon {
font-size: 80rpx;
opacity: 0.5;
}
.status-title {
font-size: 32rpx;
font-weight: 600;
color: #374151;
}
.status-subtitle {
font-size: 24rpx;
color: #6B7280;
line-height: 1.5;
max-width: 400rpx;
}
.retry-button {
background: linear-gradient(135deg, #05C7C7 0%, #04B5B5 100%);
color: #ffffff;
padding: 24rpx 48rpx;
border-radius: 32rpx;
margin-top: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(5, 199, 199, 0.2);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:active {
transform: scale(0.95);
}
.retry-text {
font-size: 28rpx;
font-weight: 600;
}
}
}
}
// Venue Cards with premium styling
.venue-section {
padding: 0 32rpx;
@ -255,7 +343,7 @@
// Timeline
.timeline-container {
position: absolute;
left: 80rpx;
left: 60rpx;
top: 0;
bottom: 0;
width: 2rpx;
@ -265,7 +353,7 @@
top: 0;
bottom: 0;
width: 2rpx;
background: linear-gradient(180deg, rgba(5, 199, 199, 0.3) 0%, #E5E7EB 30%, #E5E7EB 100%);
background: linear-gradient(180deg, rgba(5, 199, 199, 0.3) 0%, #E5E7EB 30%, #E5E7EB 80%, rgba(229, 231, 235, 0) 100%);
}
}
@ -275,7 +363,7 @@
.timeline-dot {
position: absolute;
left: 38rpx;
left: 18rpx;
top: 10rpx;
width: 24rpx;
height: 24rpx;
@ -292,7 +380,7 @@
}
.venue-meta {
margin-left: 96rpx;
margin-left: 76rpx;
margin-bottom: 24rpx;
display: flex;
align-items: center;
@ -328,7 +416,7 @@
}
.venue-content {
margin-left: 96rpx;
margin-left: 76rpx;
background: linear-gradient(135deg, #ffffff 0%, rgba(248, 250, 252, 0.5) 100%);
border-radius: 48rpx;
overflow: hidden;
@ -962,7 +1050,7 @@
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
.stat-card {
@ -971,6 +1059,14 @@
padding: 24rpx;
text-align: center;
border: 2rpx solid #F3F4F6;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
&:active {
background: #F9FAFB;
transform: scale(0.98);
border-color: #05C7C7;
}
.stat-number {
font-size: 48rpx;
@ -1150,17 +1246,24 @@
}
}
.activity-points {
font-size: 20rpx;
color: #05C7C7;
font-weight: 600;
background: #F0FDFD;
padding: 6rpx 12rpx;
border-radius: 12rpx;
border: 1rpx solid #CCFBF1;
white-space: nowrap;
margin-top: 2rpx;
}
}
}
.activity-more-link {
margin-top: 24rpx;
text-align: center;
padding: 16rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:active {
transform: scale(0.98);
}
.more-link-text {
font-size: 26rpx;
color: #05C7C7;
font-weight: 500;
}
}
}

View file

@ -57,13 +57,35 @@
<view class="location-bar">
<view class="location-content">
<text class="location-pin">📍</text>
<text class="location-text">杭州市文一西路未来科技城万利大厦0701</text>
<text class="location-switch" @tap="switchLocation">切换</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>
<!-- Venue Cards -->
<view class="venue-section">
<!-- 根据定位状态显示不同内容 -->
<!-- 定位中状态 -->
<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>
@ -215,18 +237,14 @@
<!-- Stats Cards -->
<view class="stats-section">
<view class="stats-grid">
<view class="stat-card">
<view class="stat-card" @tap="goToHistory">
<text class="stat-number">12</text>
<text class="stat-label">观看场次</text>
<text class="stat-label">观看数量</text>
</view>
<view class="stat-card">
<view class="stat-card" @tap="goToFavorites">
<text class="stat-number">5</text>
<text class="stat-label">收藏场馆</text>
</view>
<view class="stat-card">
<text class="stat-number">28</text>
<text class="stat-label">观看时长(h)</text>
</view>
</view>
</view>
@ -235,7 +253,7 @@
<view class="menu-card">
<view class="menu-item" @tap="goToFavorites">
<view class="menu-content">
<text class="menu-title">我的下载</text>
<text class="menu-title">我的收藏</text>
</view>
<text class="menu-arrow"></text>
</view>
@ -256,6 +274,13 @@
<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>
@ -272,16 +297,17 @@
<text class="activity-title-text">观看了 RONIN黄金篮球馆</text>
<text class="activity-subtitle">2小时前 · 观看时长 45分钟</text>
</view>
<text class="activity-points">+10</text>
</view>
<view class="activity-item">
<view class="activity-content">
<text class="activity-title-text">收藏了 Panda惊怒熊猫运动俱乐部</text>
<text class="activity-subtitle">1天前</text>
</view>
<text class="activity-points">+5</text>
</view>
</view>
<view class="activity-more-link" @tap="goToActivityHistory">
<text class="more-link-text">查看历史活动</text>
</view>
</view>
</view>
</view>
@ -304,12 +330,23 @@
<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 } from 'vue'
import { ref, onMounted, defineAsyncComponent } from 'vue'
import Taro from '@tarojs/taro'
// AuthModal
const AuthModal = defineAsyncComponent(() => import('../../components/AuthModal/index.vue'))
import './index.scss'
//
@ -323,27 +360,284 @@ const roninLogoSrc = 'https://qiniu.drip.im/gh_09bd6126eab8/20250711/upload/4d0e
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 方法被调用!')
Taro.showActionSheet({
itemList: ['杭州市文一西路未来科技城万利大厦0701', '杭州市西湖区文三路123号', '杭州市滨江区江南大道456号'],
//
chooseLocationOnMap()
}
//
const chooseLocationOnMap = () => {
Taro.chooseLocation({
success: (res) => {
console.log('选择位置:', res.tapIndex)
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 = () => {
Taro.showToast({
title: '播放视频',
icon: 'none'
})
if (checkAuthRequired('video')) {
Taro.showToast({
title: '播放视频',
icon: 'none'
})
}
}
const enterZone = (venueType: string) => {
@ -375,7 +669,9 @@ const goHome = () => {
}
const goProfile = () => {
currentPage.value = 'profile'
if (checkAuthRequired('login')) {
currentPage.value = 'profile'
}
}
//
@ -399,16 +695,14 @@ const goToVip = () => {
}
const goToFavorites = () => {
Taro.showToast({
title: '我的收藏',
icon: 'none'
Taro.navigateTo({
url: '/pages/favorites/index'
})
}
const goToHistory = () => {
Taro.showToast({
title: '观看历史',
icon: 'none'
Taro.navigateTo({
url: '/pages/history/index'
})
}
@ -420,9 +714,20 @@ const goToAddress = () => {
}
const goToNotifications = () => {
Taro.showToast({
title: '消息通知',
icon: 'none'
Taro.navigateTo({
url: '/pages/notifications/index'
})
}
const goToOrders = () => {
Taro.navigateTo({
url: '/pages/orders/index'
})
}
const goToActivityHistory = () => {
Taro.navigateTo({
url: '/pages/activity/index'
})
}
@ -433,10 +738,98 @@ const goToPrivacy = () => {
})
}
//
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>

View file

@ -0,0 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '消息通知',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
navigationStyle: 'default'
})

View file

@ -0,0 +1,158 @@
.notifications-page {
min-height: 100vh;
background: #f8fafc;
padding-bottom: 32rpx;
}
.page-header {
background: #ffffff;
padding: 48rpx 32rpx 32rpx 32rpx;
border-bottom: 2rpx solid #f1f5f9;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
color: #1e293b;
display: block;
margin-bottom: 8rpx;
}
.header-subtitle {
font-size: 26rpx;
color: #64748b;
display: block;
}
.actions-bar {
background: #ffffff;
padding: 16rpx 32rpx;
border-bottom: 2rpx solid #f1f5f9;
display: flex;
justify-content: flex-end;
gap: 24rpx;
}
.action-btn {
background: #f8fafc;
color: #64748b;
padding: 12rpx 24rpx;
border-radius: 16rpx;
border: 2rpx solid #e2e8f0;
transition: all 0.3s ease;
}
.action-btn:active {
background: #e2e8f0;
transform: scale(0.95);
}
.action-text {
font-size: 24rpx;
font-weight: 500;
}
.empty-state {
padding: 120rpx 32rpx;
min-height: 600rpx;
display: flex;
align-items: center;
justify-content: center;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 24rpx;
}
.empty-icon {
font-size: 96rpx;
margin-bottom: 16rpx;
opacity: 0.6;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: #374151;
}
.empty-subtitle {
font-size: 26rpx;
color: #6b7280;
line-height: 1.5;
max-width: 400rpx;
}
.notifications-list {
padding: 16rpx;
}
.notification-card {
background: #ffffff;
border-radius: 16rpx;
margin-bottom: 16rpx;
padding: 24rpx;
border: 2rpx solid #f1f5f9;
position: relative;
transition: all 0.3s ease;
}
.notification-card:active {
background: #f8fafc;
transform: translateY(-1rpx);
}
.notification-card.unread {
border-color: #e0f2fe;
background: #fefeff;
}
.notification-content {
width: 100%;
}
.notification-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 8rpx;
gap: 16rpx;
}
.notification-title {
font-size: 28rpx;
font-weight: 600;
color: #1e293b;
line-height: 1.3;
}
.notification-time {
font-size: 20rpx;
color: #9ca3af;
white-space: nowrap;
margin-top: 2rpx;
}
.notification-message {
font-size: 24rpx;
color: #64748b;
line-height: 1.5;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.unread-dot {
position: absolute;
top: 24rpx;
right: 24rpx;
width: 16rpx;
height: 16rpx;
background: #ef4444;
border-radius: 50%;
}

View file

@ -0,0 +1,180 @@
<template>
<view class="notifications-page">
<!-- Header -->
<view class="page-header">
<text class="header-title">消息通知</text>
<text class="header-subtitle">{{ unreadCount > 0 ? `${unreadCount} 条未读` : '全部已读' }}</text>
</view>
<!-- Actions Bar -->
<view v-if="notifications.length > 0" class="actions-bar">
<view class="action-btn" @tap="markAllAsRead">
<text class="action-text">全部已读</text>
</view>
<view class="action-btn" @tap="clearAllNotifications">
<text class="action-text">清空通知</text>
</view>
</view>
<!-- Empty State -->
<view v-if="notifications.length === 0" class="empty-state">
<view class="empty-container">
<text class="empty-icon">🔔</text>
<text class="empty-title">暂无消息通知</text>
<text class="empty-subtitle">有新消息时会在这里显示</text>
</view>
</view>
<!-- Notifications List -->
<view v-else class="notifications-list">
<view
v-for="notification in notifications"
:key="notification.id"
class="notification-card"
:class="{ unread: !notification.isRead }"
@tap="markAsRead(notification)"
>
<!-- Notification Content -->
<view class="notification-content">
<view class="notification-header">
<text class="notification-title">{{ notification.title }}</text>
<text class="notification-time">{{ notification.time }}</text>
</view>
<text class="notification-message">{{ notification.message }}</text>
</view>
<!-- Unread Indicator -->
<view v-if="!notification.isRead" class="unread-dot"></view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import './index.scss'
//
interface Notification {
id: string
type: 'system' | 'activity' | 'interaction' | 'live'
title: string
message: string
time: string
isRead: boolean
}
//
const notifications = ref<Notification[]>([
{
id: '1',
type: 'system',
title: '系统消息',
message: '您的会员权益即将到期,请及时续费以享受更多服务',
time: '10分钟前',
isRead: false
},
{
id: '2',
type: 'live',
title: 'RONIN黄金篮球馆',
message: '精彩比赛正在直播中,快来观看吧!',
time: '30分钟前',
isRead: false
},
{
id: '3',
type: 'activity',
title: '活动通知',
message: '新人注册送积分活动开始啦,邀请好友一起来参与',
time: '2小时前',
isRead: true
},
{
id: '4',
type: 'interaction',
title: '互动消息',
message: '有用户关注了您,快去查看吧',
time: '1天前',
isRead: true
},
{
id: '5',
type: 'system',
title: '版本更新',
message: 'LIMO来刻已更新至最新版本新增多项实用功能',
time: '2天前',
isRead: false
},
{
id: '6',
type: 'live',
title: 'Panda惊怒熊猫运动俱乐部',
message: '今日训练课程直播已结束,感谢观看',
time: '3天前',
isRead: true
}
])
//
const unreadCount = computed(() => {
return notifications.value.filter(n => !n.isRead).length
})
//
const markAsRead = (notification: Notification) => {
if (!notification.isRead) {
notification.isRead = true
Taro.showToast({
title: '已标记为已读',
icon: 'none',
duration: 1000
})
}
}
//
const markAllAsRead = () => {
const unreadNotifications = notifications.value.filter(n => !n.isRead)
if (unreadNotifications.length === 0) {
Taro.showToast({
title: '没有未读消息',
icon: 'none'
})
return
}
notifications.value.forEach(n => {
n.isRead = true
})
Taro.showToast({
title: '已全部标记为已读',
icon: 'success'
})
}
//
const clearAllNotifications = () => {
Taro.showModal({
title: '清空通知',
content: '确定要清空所有消息通知吗?此操作不可恢复。',
success: (res) => {
if (res.confirm) {
notifications.value = []
Taro.showToast({
title: '已清空所有通知',
icon: 'success'
})
}
}
})
}
onMounted(() => {
console.log('消息通知页面已加载')
})
</script>

View file

@ -0,0 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '我的订单',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
navigationStyle: 'default'
})

View file

@ -0,0 +1,323 @@
.orders-page {
min-height: 100vh;
background: #f9fafb;
// Status Tabs
.status-tabs {
background: #ffffff;
padding: 24rpx 24rpx 0 24rpx;
display: flex;
border-bottom: 2rpx solid #f3f4f6;
position: sticky;
top: 0;
z-index: 10;
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 16rpx;
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:active {
transform: scale(0.98);
}
.tab-text {
font-size: 28rpx;
color: #6b7280;
font-weight: 500;
transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.tab-badge {
position: absolute;
top: 12rpx;
right: 12rpx;
background: #ef4444;
border-radius: 20rpx;
min-width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 8rpx;
.badge-text {
color: #ffffff;
font-size: 20rpx;
font-weight: 600;
}
}
&.active {
.tab-text {
color: #05c7c7;
font-weight: 600;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 64rpx;
height: 6rpx;
background: linear-gradient(135deg, #05c7c7 0%, #04b5b5 100%);
border-radius: 6rpx;
}
}
}
}
// Empty State
.empty-state {
padding: 120rpx 48rpx;
display: flex;
justify-content: center;
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
.empty-icon {
font-size: 120rpx;
margin-bottom: 32rpx;
opacity: 0.6;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: #374151;
margin-bottom: 16rpx;
}
.empty-subtitle {
font-size: 26rpx;
color: #9ca3af;
margin-bottom: 48rpx;
line-height: 1.5;
}
.empty-button {
background: linear-gradient(135deg, #05c7c7 0%, #04b5b5 100%);
color: #ffffff;
padding: 20rpx 40rpx;
border-radius: 28rpx;
box-shadow: 0 8rpx 24rpx rgba(5, 199, 199, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:active {
transform: scale(0.98);
}
.button-text {
font-size: 26rpx;
font-weight: 600;
}
}
}
}
// Orders List
.orders-list {
padding: 24rpx;
.order-card {
background: #ffffff;
border-radius: 24rpx;
border: 2rpx solid #f3f4f6;
margin-bottom: 24rpx;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:active {
transform: scale(0.995);
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
}
&:last-child {
margin-bottom: 0;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 32rpx 32rpx 24rpx 32rpx;
border-bottom: 2rpx solid #f9fafb;
.order-info {
display: flex;
flex-direction: column;
.order-number {
font-size: 26rpx;
color: #6b7280;
margin-bottom: 8rpx;
}
.order-date {
font-size: 24rpx;
color: #9ca3af;
}
}
.order-status {
.status-text {
font-size: 24rpx;
font-weight: 600;
}
&.pending {
.status-text {
color: #d97706;
}
}
&.completed {
.status-text {
color: #047857;
}
}
&.cancelled {
.status-text {
color: #dc2626;
}
}
}
}
.order-content {
padding: 24rpx 32rpx 32rpx 32rpx;
.member-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
.member-card {
display: flex;
align-items: center;
flex: 1;
.card-icon {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
.icon-text {
font-size: 36rpx;
}
}
.member-details {
display: flex;
flex-direction: column;
.member-type {
font-size: 28rpx;
font-weight: 600;
color: #111827;
margin-bottom: 6rpx;
}
.member-duration {
font-size: 24rpx;
color: #6b7280;
}
}
}
.order-amount {
display: flex;
flex-direction: column;
align-items: flex-end;
.amount-label {
font-size: 22rpx;
color: #9ca3af;
margin-bottom: 6rpx;
}
.amount-value {
font-size: 32rpx;
font-weight: 700;
color: #ef4444;
}
}
}
.order-actions {
display: flex;
gap: 16rpx;
justify-content: flex-end;
.action-btn {
padding: 16rpx 24rpx;
border-radius: 20rpx;
border: 2rpx solid;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:active {
transform: scale(0.95);
}
.btn-text {
font-size: 24rpx;
font-weight: 600;
}
&.cancel-btn {
background: #ffffff;
border-color: #d1d5db;
.btn-text {
color: #6b7280;
}
&:active {
background: #f9fafb;
}
}
&.pay-btn {
background: linear-gradient(135deg, #05c7c7 0%, #04b5b5 100%);
border-color: #05c7c7;
box-shadow: 0 4rpx 16rpx rgba(5, 199, 199, 0.3);
.btn-text {
color: #ffffff;
}
}
&.reorder-btn {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border-color: #10b981;
box-shadow: 0 4rpx 16rpx rgba(16, 185, 129, 0.3);
.btn-text {
color: #ffffff;
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,310 @@
<template>
<view class="orders-page">
<!-- Header Tabs -->
<view class="status-tabs">
<view
v-for="tab in statusTabs"
:key="tab.key"
class="tab-item"
:class="{ active: activeTab === tab.key }"
@tap="setActiveTab(tab.key)"
>
<text class="tab-text">{{ tab.label }}</text>
<view v-if="tab.count > 0" class="tab-badge">
<text class="badge-text">{{ tab.count }}</text>
</view>
</view>
</view>
<!-- Empty State -->
<view v-if="filteredOrders.length === 0" class="empty-state">
<view class="empty-container">
<text class="empty-icon">📋</text>
<text class="empty-title">暂无相关订单</text>
<text class="empty-subtitle">{{ getEmptyMessage() }}</text>
<view class="empty-button" @tap="goToVip">
<text class="button-text">立即开通会员</text>
</view>
</view>
</view>
<!-- Orders List -->
<view v-else class="orders-list">
<view
v-for="order in filteredOrders"
:key="order.id"
class="order-card"
@tap="viewOrderDetail(order)"
>
<!-- Order Header -->
<view class="order-header">
<view class="order-info">
<text class="order-number">订单号{{ order.orderNumber }}</text>
<text class="order-date">{{ order.createTime }}</text>
</view>
<view class="order-status" :class="order.status">
<text class="status-text">{{ getStatusText(order.status) }}</text>
</view>
</view>
<!-- Order Content -->
<view class="order-content">
<view class="member-info">
<view class="member-card">
<view class="card-icon">
<text class="icon-text">👑</text>
</view>
<view class="member-details">
<text class="member-type">{{ order.memberType }}</text>
<text class="member-duration">{{ order.duration }}</text>
</view>
</view>
<view class="order-amount">
<text class="amount-label">实付金额</text>
<text class="amount-value">¥{{ order.amount }}</text>
</view>
</view>
<!-- Order Actions -->
<view class="order-actions">
<view
v-if="order.status === 'pending'"
class="action-btn cancel-btn"
@tap.stop="cancelOrder(order)"
>
<text class="btn-text">取消订单</text>
</view>
<view
v-if="order.status === 'pending'"
class="action-btn pay-btn"
@tap.stop="payOrder(order)"
>
<text class="btn-text">立即支付</text>
</view>
<view
v-if="order.status === 'cancelled'"
class="action-btn reorder-btn"
@tap.stop="reorder(order)"
>
<text class="btn-text">重新购买</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import './index.scss'
//
type OrderStatus = 'all' | 'pending' | 'completed' | 'cancelled'
//
const statusTabs = ref([
{ key: 'all' as OrderStatus, label: '全部', count: 0 },
{ key: 'pending' as OrderStatus, label: '待支付', count: 0 },
{ key: 'completed' as OrderStatus, label: '已完成', count: 0 },
{ key: 'cancelled' as OrderStatus, label: '已取消', count: 0 }
])
//
const activeTab = ref<OrderStatus>('all')
//
const orders = ref([
{
id: 'order001',
orderNumber: '202501120001',
memberType: 'LIMO VIP黄金会员',
duration: '12个月',
amount: '998.00',
status: 'completed',
createTime: '2025-01-10 14:30',
payTime: '2025-01-10 14:35'
},
{
id: 'order002',
orderNumber: '202501110001',
memberType: 'LIMO VIP白银会员',
duration: '6个月',
amount: '598.00',
status: 'pending',
createTime: '2025-01-11 16:20',
payTime: ''
},
{
id: 'order003',
orderNumber: '202501090001',
memberType: 'LIMO VIP黄金会员',
duration: '3个月',
amount: '299.00',
status: 'cancelled',
createTime: '2025-01-09 10:15',
payTime: ''
},
{
id: 'order004',
orderNumber: '202501080001',
memberType: 'LIMO VIP白银会员',
duration: '1个月',
amount: '99.00',
status: 'completed',
createTime: '2025-01-08 11:45',
payTime: '2025-01-08 11:50'
},
{
id: 'order005',
orderNumber: '202501070001',
memberType: 'LIMO VIP黄金会员',
duration: '6个月',
amount: '599.00',
status: 'pending',
createTime: '2025-01-07 09:30',
payTime: ''
}
])
//
const filteredOrders = computed(() => {
if (activeTab.value === 'all') {
return orders.value
}
return orders.value.filter(order => order.status === activeTab.value)
})
//
const updateTabCounts = () => {
statusTabs.value.forEach(tab => {
if (tab.key === 'all') {
tab.count = orders.value.length
} else {
tab.count = orders.value.filter(order => order.status === tab.key).length
}
})
}
//
const setActiveTab = (tabKey: OrderStatus) => {
activeTab.value = tabKey
}
const getStatusText = (status: string) => {
const statusMap = {
'pending': '待支付',
'completed': '已完成',
'cancelled': '已取消'
}
return statusMap[status] || status
}
const getEmptyMessage = () => {
const messageMap = {
'all': '您还没有任何订单',
'pending': '暂无待支付订单',
'completed': '暂无已完成订单',
'cancelled': '暂无已取消订单'
}
return messageMap[activeTab.value] || '暂无相关订单'
}
const viewOrderDetail = (order: any) => {
Taro.showModal({
title: '订单详情',
content: `订单号:${order.orderNumber}\n会员类型${order.memberType}\n订单金额¥${order.amount}\n创建时间${order.createTime}`,
showCancel: false
})
}
const payOrder = (order: any) => {
Taro.showModal({
title: '支付订单',
content: `确认支付 ¥${order.amount} 购买${order.memberType}`,
success: (res) => {
if (res.confirm) {
//
Taro.showLoading({ title: '支付中...' })
setTimeout(() => {
Taro.hideLoading()
//
const orderIndex = orders.value.findIndex(o => o.id === order.id)
if (orderIndex > -1) {
orders.value[orderIndex].status = 'completed'
orders.value[orderIndex].payTime = new Date().toLocaleString('zh-CN')
updateTabCounts()
}
Taro.showToast({
title: '支付成功',
icon: 'success'
})
}, 2000)
}
}
})
}
const cancelOrder = (order: any) => {
Taro.showModal({
title: '取消订单',
content: '确定要取消这个订单吗?',
success: (res) => {
if (res.confirm) {
//
const orderIndex = orders.value.findIndex(o => o.id === order.id)
if (orderIndex > -1) {
orders.value[orderIndex].status = 'cancelled'
updateTabCounts()
}
Taro.showToast({
title: '订单已取消',
icon: 'success'
})
}
}
})
}
const reorder = (order: any) => {
Taro.showModal({
title: '重新购买',
content: `重新购买${order.memberType}`,
success: (res) => {
if (res.confirm) {
//
const newOrder = {
...order,
id: 'order' + Date.now(),
orderNumber: new Date().toISOString().slice(0, 10).replace(/-/g, '') + Date.now().toString().slice(-4),
status: 'pending',
createTime: new Date().toLocaleString('zh-CN'),
payTime: ''
}
orders.value.unshift(newOrder)
updateTabCounts()
activeTab.value = 'pending'
Taro.showToast({
title: '订单已创建',
icon: 'success'
})
}
}
})
}
const goToVip = () => {
Taro.navigateTo({
url: '/pages/vip/index'
})
}
onMounted(() => {
updateTabCounts()
console.log('我的订单页面已加载')
})
</script>

View file

@ -13,42 +13,12 @@
<!-- VIP Banner -->
<view class="vip-banner">
<!-- Background decorations -->
<view class="decoration decoration-1"></view>
<view class="decoration decoration-2"></view>
<view class="decoration decoration-3"></view>
<view class="banner-content">
<view class="crown-container">
<text class="crown-icon">👑</text>
</view>
<text class="banner-title">LIMO VIP会员</text>
<text class="banner-subtitle">开启专属运动体验</text>
<!-- Stats -->
<view class="stats-container">
<view class="stat-item">
<view class="stat-icon-container">
<text class="stat-icon">👥</text>
</view>
<text class="stat-value">50+</text>
<text class="stat-label">活跃用户</text>
</view>
<view class="stat-item">
<view class="stat-icon-container">
<text class="stat-icon"></text>
</view>
<text class="stat-value">98%</text>
<text class="stat-label">满意度</text>
</view>
<view class="stat-item">
<view class="stat-icon-container">
<text class="stat-icon">🏆</text>
</view>
<text class="stat-value">1000+</text>
<text class="stat-label">场馆覆盖</text>
</view>
</view>
<text class="banner-subtitle">解锁专属运动特权</text>
</view>
</view>
@ -128,46 +98,7 @@
</view>
</view>
<!-- Testimonials -->
<view class="testimonials-section">
<view class="testimonials-card">
<text class="testimonials-title">用户好评</text>
<view class="testimonials-list">
<view class="testimonial-item blue">
<view class="testimonial-header">
<view class="user-avatar blue-avatar">
<text class="avatar-text"></text>
</view>
<text class="user-name">张先生</text>
<view class="rating">
<text class="star"></text>
<text class="star"></text>
<text class="star"></text>
<text class="star"></text>
<text class="star"></text>
</view>
</view>
<text class="testimonial-text">"VIP服务真的很棒预约场馆再也不用排队了"</text>
</view>
<view class="testimonial-item green">
<view class="testimonial-header">
<view class="user-avatar green-avatar">
<text class="avatar-text"></text>
</view>
<text class="user-name">李女士</text>
<view class="rating">
<text class="star"></text>
<text class="star"></text>
<text class="star"></text>
<text class="star"></text>
<text class="star"></text>
</view>
</view>
<text class="testimonial-text">"专属客服响应很快,解决问题很及时,值得推荐!"</text>
</view>
</view>
</view>
</view>
<!-- Purchase Section -->
<view class="purchase-section">
@ -185,20 +116,6 @@
<text class="purchase-text">立即开通VIP会员</text>
</view>
<view class="purchase-features">
<view class="feature-item">
<text class="feature-icon">🛡</text>
<text class="feature-text">安全支付</text>
</view>
<view class="feature-divider"></view>
<view class="feature-item">
<text class="feature-icon"></text>
<text class="feature-text">随时取消</text>
</view>
<view class="feature-divider"></view>
<text class="feature-text">7天无理由退款</text>
</view>
<text class="agreement-text">开通即表示同意VIP会员服务协议</text>
</view>
</view>
@ -217,20 +134,10 @@ import './index.scss'
const backIcon = require('@/assets/images/back.svg')
//
const selectedPlan = ref('monthly')
const selectedPlan = ref('quarterly')
//
const plans = ref([
{
id: 'daily',
name: '体验版',
duration: '1天',
price: 9.9,
originalPrice: 19.9,
discount: '限时5折',
popular: false,
colorClass: 'blue-gradient',
},
{
id: 'monthly',
name: '月度会员',
@ -238,7 +145,7 @@ const plans = ref([
price: 68,
originalPrice: 98,
discount: '7折优惠',
popular: true,
popular: false,
colorClass: 'purple-gradient',
},
{
@ -247,9 +154,9 @@ const plans = ref([
duration: '3个月',
price: 168,
originalPrice: 294,
discount: '超值4.3折',
popular: false,
colorClass: 'pink-gradient',
discount: '超值优惠',
popular: true,
colorClass: 'orange-gradient',
},
{
id: 'yearly',
@ -257,50 +164,32 @@ const plans = ref([
duration: '1年',
price: 588,
originalPrice: 1176,
discount: '超值5折',
discount: '最划算',
popular: false,
colorClass: 'orange-gradient',
colorClass: 'pink-gradient',
},
])
//
const privileges = ref([
{
icon: '👑',
title: '专属身份标识',
desc: 'VIP专属标识彰显尊贵身份',
colorClass: 'yellow-gradient',
},
{
icon: '⚡',
title: '优先预约权',
desc: '热门场馆优先预约,不再排队等待',
colorClass: 'blue-gradient',
colorClass: 'orange-gradient',
},
{
icon: '🎁',
title: '专属折扣',
desc: '场馆预订享受VIP专属折扣优惠',
colorClass: 'green-gradient',
colorClass: 'purple-gradient',
},
{
icon: '🛡️',
title: '免费取消',
desc: '预约后可免费取消,无违约金',
colorClass: 'purple-gradient',
},
{
icon: '⭐',
title: '专属客服',
desc: '7x24小时VIP专属客服服务',
colorClass: 'pink-gradient',
},
{
icon: '✓',
title: '高清回放',
desc: '无限制观看高清比赛回放视频',
colorClass: 'indigo-gradient',
},
])
//

View file

@ -15,7 +15,11 @@
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #1f2937 0%, #3b82f6 50%, #ef4444 100%);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: blur(2rpx);
transform: scale(1.1);
z-index: 1;
}
@ -25,7 +29,7 @@
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
background: rgba(0, 0, 0, 0.6);
z-index: 2;
}
@ -35,7 +39,7 @@
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.2) 0%, transparent 40%, rgba(0, 0, 0, 0.6) 100%);
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, transparent 30%, rgba(0, 0, 0, 0.8) 100%);
z-index: 3;
}
@ -51,7 +55,7 @@
.control-btn {
width: 64rpx;
height: 64rpx;
background: rgba(0, 0, 0, 0.3);
background: rgba(255, 255, 255, 0.15);
border-radius: 24rpx;
display: flex;
align-items: center;
@ -174,6 +178,9 @@
padding: 12rpx 24rpx;
box-shadow: 0 8rpx 24rpx rgba(5, 199, 199, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
&:active {
transform: scale(0.95);
@ -183,6 +190,7 @@
font-size: 24rpx;
color: #ffffff;
font-weight: 600;
line-height: 1;
}
}

View file

@ -2,7 +2,7 @@
<view class="zone-venue">
<!-- Header with premium background -->
<view class="hero-header">
<view class="hero-bg"></view>
<view class="hero-bg" :style="{ 'background-image': `url(${roninLogoSrc})` }"></view>
<view class="hero-overlay"></view>
<view class="hero-gradient"></view>
@ -155,11 +155,20 @@
<!-- Bottom Spacing -->
<view class="bottom-spacing"></view>
<!-- 授权弹窗 -->
<AuthModal
:visible="authModalVisible"
:auth-type="authType"
@close="closeAuthModal"
@success="onAuthSuccess"
@skip="onAuthSkip"
/>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, defineAsyncComponent } from 'vue'
import Taro from '@tarojs/taro'
import './index.scss'
@ -169,9 +178,16 @@ const shareIcon = require('@/assets/images/share.svg')
const moreIcon = require('@/assets/images/more.svg')
const roninLogoSrc = 'https://qiniu.drip.im/gh_09bd6126eab8/20250711/upload/4d0efe5313d38f0db01d0cfba4dac9d848bb43a2'
//
const AuthModal = defineAsyncComponent(() => import('../../components/AuthModal/index.vue'))
//
const activeTab = ref('highlights')
//
const authModalVisible = ref(false)
const authType = ref<'login' | 'video'>('video')
//
const goBack = () => {
Taro.navigateBack()
@ -192,13 +208,59 @@ const showMore = () => {
})
}
const playMainVideo = () => {
//
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)
//
if (data.phone) {
Taro.setStorageSync('userPhone', data.phone)
}
if (data.userInfo) {
Taro.setStorageSync('userInfo', data.userInfo)
}
Taro.showToast({
title: '播放主视频',
icon: 'none'
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
}
return true
}
const playMainVideo = () => {
if (checkAuthRequired('video')) {
Taro.showToast({
title: '播放主视频',
icon: 'none'
})
}
}
const toggleFullscreen = () => {
Taro.showToast({
title: '全屏播放',
@ -227,10 +289,12 @@ const selectTime = () => {
}
const playGridVideo = (index: number) => {
Taro.showToast({
title: `播放视频 ${index + 1}`,
icon: 'none'
})
if (checkAuthRequired('video')) {
Taro.showToast({
title: `播放视频 ${index + 1}`,
icon: 'none'
})
}
}
const createClip = () => {

75
client/src/utils/auth.ts Normal file
View file

@ -0,0 +1,75 @@
import Taro from '@tarojs/taro'
// 授权状态接口
export interface AuthStatus {
hasPhone: boolean
hasUserInfo: boolean
phone?: string
userInfo?: any
}
// 检查用户授权状态
export const checkAuthStatus = (): AuthStatus => {
const phone = Taro.getStorageSync('userPhone')
const userInfo = Taro.getStorageSync('userInfo')
return {
hasPhone: !!phone,
hasUserInfo: !!userInfo,
phone,
userInfo
}
}
// 保存用户授权信息
export const saveAuthInfo = (data: { phone?: string, userInfo?: any }) => {
if (data.phone) {
Taro.setStorageSync('userPhone', data.phone)
}
if (data.userInfo) {
Taro.setStorageSync('userInfo', data.userInfo)
}
}
// 清除用户授权信息
export const clearAuthInfo = () => {
Taro.removeStorageSync('userPhone')
Taro.removeStorageSync('userInfo')
}
// 检查是否需要授权
export const isAuthRequired = (): boolean => {
const status = checkAuthStatus()
return !status.hasPhone || !status.hasUserInfo
}
// 获取用户显示名称
export const getUserDisplayName = (): string => {
const status = checkAuthStatus()
if (status.userInfo && status.userInfo.nickName) {
return status.userInfo.nickName
}
return '用户昵称'
}
// 获取用户头像
export const getUserAvatar = (): string => {
const status = checkAuthStatus()
if (status.userInfo && status.userInfo.avatarUrl) {
return status.userInfo.avatarUrl
}
return ''
}
// 格式化手机号显示
export const formatPhoneDisplay = (): string => {
const status = checkAuthStatus()
if (status.phone) {
// 如果是完整手机号,进行脱敏处理
if (status.phone.length === 11) {
return status.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
return status.phone
}
return '未授权'
}