save
This commit is contained in:
parent
dc24d5b6ed
commit
cd11b355bb
25 changed files with 3632 additions and 186 deletions
|
@ -2,7 +2,12 @@ export default defineAppConfig({
|
||||||
pages: [
|
pages: [
|
||||||
'pages/index/index',
|
'pages/index/index',
|
||||||
'pages/zone/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: {
|
window: {
|
||||||
backgroundTextStyle: 'light',
|
backgroundTextStyle: 'light',
|
||||||
|
@ -10,5 +15,17 @@ export default defineAppConfig({
|
||||||
navigationBarTitleText: 'LIMO来刻',
|
navigationBarTitleText: 'LIMO来刻',
|
||||||
navigationBarTextStyle: 'white',
|
navigationBarTextStyle: 'white',
|
||||||
navigationStyle: 'custom'
|
navigationStyle: 'custom'
|
||||||
|
},
|
||||||
|
// 声明需要的隐私接口
|
||||||
|
requiredPrivateInfos: [
|
||||||
|
'getLocation',
|
||||||
|
'chooseLocation',
|
||||||
|
'chooseAddress'
|
||||||
|
],
|
||||||
|
// 位置权限说明
|
||||||
|
permission: {
|
||||||
|
'scope.userLocation': {
|
||||||
|
desc: '您的位置信息将用于推荐附近的运动场馆'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
580
client/src/components/AuthModal/index.scss
Normal file
580
client/src/components/AuthModal/index.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
299
client/src/components/AuthModal/index.vue
Normal file
299
client/src/components/AuthModal/index.vue
Normal 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>
|
|
@ -2,3 +2,4 @@
|
||||||
* 组件导出
|
* 组件导出
|
||||||
*/
|
*/
|
||||||
export { default as CommonButton } from './CommonButton/index.vue'
|
export { default as CommonButton } from './CommonButton/index.vue'
|
||||||
|
export { default as AuthModal } from './AuthModal/index.vue'
|
6
client/src/pages/activity/index.config.ts
Normal file
6
client/src/pages/activity/index.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '历史活动',
|
||||||
|
navigationBarBackgroundColor: '#ffffff',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationStyle: 'default'
|
||||||
|
})
|
137
client/src/pages/activity/index.scss
Normal file
137
client/src/pages/activity/index.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
202
client/src/pages/activity/index.vue
Normal file
202
client/src/pages/activity/index.vue
Normal 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>
|
6
client/src/pages/favorites/index.config.ts
Normal file
6
client/src/pages/favorites/index.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '我的收藏',
|
||||||
|
navigationBarBackgroundColor: '#ffffff',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationStyle: 'default'
|
||||||
|
})
|
166
client/src/pages/favorites/index.scss
Normal file
166
client/src/pages/favorites/index.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
113
client/src/pages/favorites/index.vue
Normal file
113
client/src/pages/favorites/index.vue
Normal 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>
|
6
client/src/pages/history/index.config.ts
Normal file
6
client/src/pages/history/index.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '观看历史',
|
||||||
|
navigationBarBackgroundColor: '#ffffff',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationStyle: 'default'
|
||||||
|
})
|
233
client/src/pages/history/index.scss
Normal file
233
client/src/pages/history/index.scss
Normal 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;
|
||||||
|
}
|
165
client/src/pages/history/index.vue
Normal file
165
client/src/pages/history/index.vue
Normal 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>
|
|
@ -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 {
|
.limo-mobile {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
@ -237,6 +256,12 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: #6B7280;
|
color: #6B7280;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
|
|
||||||
|
&.location-loading {
|
||||||
|
color: #9CA3AF;
|
||||||
|
opacity: 0.8;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-switch {
|
.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 Cards with premium styling
|
||||||
.venue-section {
|
.venue-section {
|
||||||
padding: 0 32rpx;
|
padding: 0 32rpx;
|
||||||
|
@ -255,7 +343,7 @@
|
||||||
// Timeline
|
// Timeline
|
||||||
.timeline-container {
|
.timeline-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 80rpx;
|
left: 60rpx;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 2rpx;
|
width: 2rpx;
|
||||||
|
@ -265,7 +353,7 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 2rpx;
|
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 {
|
.timeline-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 38rpx;
|
left: 18rpx;
|
||||||
top: 10rpx;
|
top: 10rpx;
|
||||||
width: 24rpx;
|
width: 24rpx;
|
||||||
height: 24rpx;
|
height: 24rpx;
|
||||||
|
@ -292,7 +380,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.venue-meta {
|
.venue-meta {
|
||||||
margin-left: 96rpx;
|
margin-left: 76rpx;
|
||||||
margin-bottom: 24rpx;
|
margin-bottom: 24rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -328,7 +416,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.venue-content {
|
.venue-content {
|
||||||
margin-left: 96rpx;
|
margin-left: 76rpx;
|
||||||
background: linear-gradient(135deg, #ffffff 0%, rgba(248, 250, 252, 0.5) 100%);
|
background: linear-gradient(135deg, #ffffff 0%, rgba(248, 250, 252, 0.5) 100%);
|
||||||
border-radius: 48rpx;
|
border-radius: 48rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -962,7 +1050,7 @@
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 24rpx;
|
gap: 24rpx;
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
|
@ -971,6 +1059,14 @@
|
||||||
padding: 24rpx;
|
padding: 24rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 2rpx solid #F3F4F6;
|
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 {
|
.stat-number {
|
||||||
font-size: 48rpx;
|
font-size: 48rpx;
|
||||||
|
@ -1150,18 +1246,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,13 +57,35 @@
|
||||||
<view class="location-bar">
|
<view class="location-bar">
|
||||||
<view class="location-content">
|
<view class="location-content">
|
||||||
<text class="location-pin">📍</text>
|
<text class="location-pin">📍</text>
|
||||||
<text class="location-text">杭州市文一西路未来科技城万利大厦0701</text>
|
<text class="location-text" :class="{ 'location-loading': locationLoading }">{{ currentLocation.address }}</text>
|
||||||
<text class="location-switch" @tap="switchLocation">切换</text>
|
<text class="location-switch" @tap="switchLocation">{{ locationLoading ? '定位中' : (currentLocation.address.includes('请选择') ? '选择' : '切换') }}</text>
|
||||||
</view>
|
</view>
|
||||||
</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 -->
|
<!-- Timeline -->
|
||||||
<view class="timeline-container">
|
<view class="timeline-container">
|
||||||
<view class="timeline-line"></view>
|
<view class="timeline-line"></view>
|
||||||
|
@ -215,18 +237,14 @@
|
||||||
<!-- Stats Cards -->
|
<!-- Stats Cards -->
|
||||||
<view class="stats-section">
|
<view class="stats-section">
|
||||||
<view class="stats-grid">
|
<view class="stats-grid">
|
||||||
<view class="stat-card">
|
<view class="stat-card" @tap="goToHistory">
|
||||||
<text class="stat-number">12</text>
|
<text class="stat-number">12</text>
|
||||||
<text class="stat-label">观看场次</text>
|
<text class="stat-label">观看数量</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-card">
|
<view class="stat-card" @tap="goToFavorites">
|
||||||
<text class="stat-number">5</text>
|
<text class="stat-number">5</text>
|
||||||
<text class="stat-label">收藏场馆</text>
|
<text class="stat-label">收藏场馆</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-card">
|
|
||||||
<text class="stat-number">28</text>
|
|
||||||
<text class="stat-label">观看时长(h)</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
@ -235,7 +253,7 @@
|
||||||
<view class="menu-card">
|
<view class="menu-card">
|
||||||
<view class="menu-item" @tap="goToFavorites">
|
<view class="menu-item" @tap="goToFavorites">
|
||||||
<view class="menu-content">
|
<view class="menu-content">
|
||||||
<text class="menu-title">我的下载</text>
|
<text class="menu-title">我的收藏</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-arrow">›</text>
|
<text class="menu-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
|
@ -256,6 +274,13 @@
|
||||||
<text class="menu-arrow">›</text>
|
<text class="menu-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
</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>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
@ -272,16 +297,17 @@
|
||||||
<text class="activity-title-text">观看了 RONIN黄金篮球馆</text>
|
<text class="activity-title-text">观看了 RONIN黄金篮球馆</text>
|
||||||
<text class="activity-subtitle">2小时前 · 观看时长 45分钟</text>
|
<text class="activity-subtitle">2小时前 · 观看时长 45分钟</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="activity-points">+10</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="activity-item">
|
<view class="activity-item">
|
||||||
<view class="activity-content">
|
<view class="activity-content">
|
||||||
<text class="activity-title-text">收藏了 Panda惊怒熊猫运动俱乐部</text>
|
<text class="activity-title-text">收藏了 Panda惊怒熊猫运动俱乐部</text>
|
||||||
<text class="activity-subtitle">1天前</text>
|
<text class="activity-subtitle">1天前</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="activity-points">+5</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="activity-more-link" @tap="goToActivityHistory">
|
||||||
|
<text class="more-link-text">查看历史活动</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
@ -304,12 +330,23 @@
|
||||||
<text class="nav-label" :class="{ active: currentPage === 'profile' }">我的</text>
|
<text class="nav-label" :class="{ active: currentPage === 'profile' }">我的</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 授权弹窗 -->
|
||||||
|
<AuthModal
|
||||||
|
:visible="authModalVisible"
|
||||||
|
:auth-type="authType"
|
||||||
|
@close="closeAuthModal"
|
||||||
|
@success="onAuthSuccess"
|
||||||
|
@skip="onAuthSkip"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, defineAsyncComponent } from 'vue'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
|
// AuthModal 组件通过相对路径导入
|
||||||
|
const AuthModal = defineAsyncComponent(() => import('../../components/AuthModal/index.vue'))
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
// 静态资源
|
// 静态资源
|
||||||
|
@ -323,28 +360,285 @@ const roninLogoSrc = 'https://qiniu.drip.im/gh_09bd6126eab8/20250711/upload/4d0e
|
||||||
const selectedSport = ref('all')
|
const selectedSport = ref('all')
|
||||||
const currentPage = ref('home') // 'home' | 'profile'
|
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) => {
|
const selectSport = (sport: string) => {
|
||||||
selectedSport.value = sport
|
selectedSport.value = sport
|
||||||
console.log('选择运动项目:', 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 = () => {
|
const switchLocation = () => {
|
||||||
console.log('🔥 switchLocation 方法被调用!')
|
console.log('🔥 switchLocation 方法被调用!')
|
||||||
Taro.showActionSheet({
|
|
||||||
itemList: ['杭州市文一西路未来科技城万利大厦0701', '杭州市西湖区文三路123号', '杭州市滨江区江南大道456号'],
|
// 直接在地图上选择位置
|
||||||
|
chooseLocationOnMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在地图上选择位置
|
||||||
|
const chooseLocationOnMap = () => {
|
||||||
|
Taro.chooseLocation({
|
||||||
success: (res) => {
|
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 = () => {
|
const playVideo = () => {
|
||||||
|
if (checkAuthRequired('video')) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '播放视频',
|
title: '播放视频',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const enterZone = (venueType: string) => {
|
const enterZone = (venueType: string) => {
|
||||||
Taro.navigateTo({
|
Taro.navigateTo({
|
||||||
|
@ -375,8 +669,10 @@ const goHome = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const goProfile = () => {
|
const goProfile = () => {
|
||||||
|
if (checkAuthRequired('login')) {
|
||||||
currentPage.value = 'profile'
|
currentPage.value = 'profile'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 我的页面功能方法
|
// 我的页面功能方法
|
||||||
const editProfile = () => {
|
const editProfile = () => {
|
||||||
|
@ -399,16 +695,14 @@ const goToVip = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToFavorites = () => {
|
const goToFavorites = () => {
|
||||||
Taro.showToast({
|
Taro.navigateTo({
|
||||||
title: '我的收藏',
|
url: '/pages/favorites/index'
|
||||||
icon: 'none'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToHistory = () => {
|
const goToHistory = () => {
|
||||||
Taro.showToast({
|
Taro.navigateTo({
|
||||||
title: '观看历史',
|
url: '/pages/history/index'
|
||||||
icon: 'none'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,9 +714,20 @@ const goToAddress = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToNotifications = () => {
|
const goToNotifications = () => {
|
||||||
Taro.showToast({
|
Taro.navigateTo({
|
||||||
title: '消息通知',
|
url: '/pages/notifications/index'
|
||||||
icon: 'none'
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => {
|
onMounted(() => {
|
||||||
console.log('LIMO来刻首页已加载')
|
console.log('LIMO来刻首页已加载')
|
||||||
console.log('Vue组件已挂载,方法可用')
|
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>
|
</script>
|
||||||
|
|
6
client/src/pages/notifications/index.config.ts
Normal file
6
client/src/pages/notifications/index.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '消息通知',
|
||||||
|
navigationBarBackgroundColor: '#ffffff',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationStyle: 'default'
|
||||||
|
})
|
158
client/src/pages/notifications/index.scss
Normal file
158
client/src/pages/notifications/index.scss
Normal 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%;
|
||||||
|
}
|
180
client/src/pages/notifications/index.vue
Normal file
180
client/src/pages/notifications/index.vue
Normal 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>
|
6
client/src/pages/orders/index.config.ts
Normal file
6
client/src/pages/orders/index.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '我的订单',
|
||||||
|
navigationBarBackgroundColor: '#ffffff',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationStyle: 'default'
|
||||||
|
})
|
323
client/src/pages/orders/index.scss
Normal file
323
client/src/pages/orders/index.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
310
client/src/pages/orders/index.vue
Normal file
310
client/src/pages/orders/index.vue
Normal 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>
|
|
@ -13,42 +13,12 @@
|
||||||
|
|
||||||
<!-- VIP Banner -->
|
<!-- VIP Banner -->
|
||||||
<view class="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="banner-content">
|
||||||
<view class="crown-container">
|
<view class="crown-container">
|
||||||
<text class="crown-icon">👑</text>
|
<text class="crown-icon">👑</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="banner-title">LIMO VIP会员</text>
|
<text class="banner-title">LIMO VIP会员</text>
|
||||||
<text class="banner-subtitle">开启专属运动体验</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>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
@ -128,46 +98,7 @@
|
||||||
</view>
|
</view>
|
||||||
</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 -->
|
<!-- Purchase Section -->
|
||||||
<view class="purchase-section">
|
<view class="purchase-section">
|
||||||
|
@ -185,20 +116,6 @@
|
||||||
<text class="purchase-text">立即开通VIP会员</text>
|
<text class="purchase-text">立即开通VIP会员</text>
|
||||||
</view>
|
</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>
|
<text class="agreement-text">开通即表示同意《VIP会员服务协议》</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
@ -217,20 +134,10 @@ import './index.scss'
|
||||||
const backIcon = require('@/assets/images/back.svg')
|
const backIcon = require('@/assets/images/back.svg')
|
||||||
|
|
||||||
// 响应式状态
|
// 响应式状态
|
||||||
const selectedPlan = ref('monthly')
|
const selectedPlan = ref('quarterly')
|
||||||
|
|
||||||
// 套餐数据
|
// 套餐数据
|
||||||
const plans = ref([
|
const plans = ref([
|
||||||
{
|
|
||||||
id: 'daily',
|
|
||||||
name: '体验版',
|
|
||||||
duration: '1天',
|
|
||||||
price: 9.9,
|
|
||||||
originalPrice: 19.9,
|
|
||||||
discount: '限时5折',
|
|
||||||
popular: false,
|
|
||||||
colorClass: 'blue-gradient',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'monthly',
|
id: 'monthly',
|
||||||
name: '月度会员',
|
name: '月度会员',
|
||||||
|
@ -238,7 +145,7 @@ const plans = ref([
|
||||||
price: 68,
|
price: 68,
|
||||||
originalPrice: 98,
|
originalPrice: 98,
|
||||||
discount: '7折优惠',
|
discount: '7折优惠',
|
||||||
popular: true,
|
popular: false,
|
||||||
colorClass: 'purple-gradient',
|
colorClass: 'purple-gradient',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -247,9 +154,9 @@ const plans = ref([
|
||||||
duration: '3个月',
|
duration: '3个月',
|
||||||
price: 168,
|
price: 168,
|
||||||
originalPrice: 294,
|
originalPrice: 294,
|
||||||
discount: '超值4.3折',
|
discount: '超值优惠',
|
||||||
popular: false,
|
popular: true,
|
||||||
colorClass: 'pink-gradient',
|
colorClass: 'orange-gradient',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'yearly',
|
id: 'yearly',
|
||||||
|
@ -257,50 +164,32 @@ const plans = ref([
|
||||||
duration: '1年',
|
duration: '1年',
|
||||||
price: 588,
|
price: 588,
|
||||||
originalPrice: 1176,
|
originalPrice: 1176,
|
||||||
discount: '超值5折',
|
discount: '最划算',
|
||||||
popular: false,
|
popular: false,
|
||||||
colorClass: 'orange-gradient',
|
colorClass: 'pink-gradient',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// 特权数据
|
// 特权数据
|
||||||
const privileges = ref([
|
const privileges = ref([
|
||||||
{
|
|
||||||
icon: '👑',
|
|
||||||
title: '专属身份标识',
|
|
||||||
desc: 'VIP专属标识,彰显尊贵身份',
|
|
||||||
colorClass: 'yellow-gradient',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: '⚡',
|
icon: '⚡',
|
||||||
title: '优先预约权',
|
title: '优先预约权',
|
||||||
desc: '热门场馆优先预约,不再排队等待',
|
desc: '热门场馆优先预约,不再排队等待',
|
||||||
colorClass: 'blue-gradient',
|
colorClass: 'orange-gradient',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '🎁',
|
icon: '🎁',
|
||||||
title: '专属折扣',
|
title: '专属折扣',
|
||||||
desc: '场馆预订享受VIP专属折扣优惠',
|
desc: '场馆预订享受VIP专属折扣优惠',
|
||||||
colorClass: 'green-gradient',
|
colorClass: 'purple-gradient',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '🛡️',
|
icon: '🛡️',
|
||||||
title: '免费取消',
|
title: '免费取消',
|
||||||
desc: '预约后可免费取消,无违约金',
|
desc: '预约后可免费取消,无违约金',
|
||||||
colorClass: 'purple-gradient',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '⭐',
|
|
||||||
title: '专属客服',
|
|
||||||
desc: '7x24小时VIP专属客服服务',
|
|
||||||
colorClass: 'pink-gradient',
|
colorClass: 'pink-gradient',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: '✓',
|
|
||||||
title: '高清回放',
|
|
||||||
desc: '无限制观看高清比赛回放视频',
|
|
||||||
colorClass: 'indigo-gradient',
|
|
||||||
},
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// 计算属性:选中的套餐信息
|
// 计算属性:选中的套餐信息
|
||||||
|
|
|
@ -15,7 +15,11 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 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;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +29,7 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(0, 0, 0, 0.4);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +39,7 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 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;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +55,7 @@
|
||||||
.control-btn {
|
.control-btn {
|
||||||
width: 64rpx;
|
width: 64rpx;
|
||||||
height: 64rpx;
|
height: 64rpx;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -174,6 +178,9 @@
|
||||||
padding: 12rpx 24rpx;
|
padding: 12rpx 24rpx;
|
||||||
box-shadow: 0 8rpx 24rpx rgba(5, 199, 199, 0.3);
|
box-shadow: 0 8rpx 24rpx rgba(5, 199, 199, 0.3);
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
|
@ -183,6 +190,7 @@
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<view class="zone-venue">
|
<view class="zone-venue">
|
||||||
<!-- Header with premium background -->
|
<!-- Header with premium background -->
|
||||||
<view class="hero-header">
|
<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-overlay"></view>
|
||||||
<view class="hero-gradient"></view>
|
<view class="hero-gradient"></view>
|
||||||
|
|
||||||
|
@ -155,11 +155,20 @@
|
||||||
|
|
||||||
<!-- Bottom Spacing -->
|
<!-- Bottom Spacing -->
|
||||||
<view class="bottom-spacing"></view>
|
<view class="bottom-spacing"></view>
|
||||||
|
|
||||||
|
<!-- 授权弹窗 -->
|
||||||
|
<AuthModal
|
||||||
|
:visible="authModalVisible"
|
||||||
|
:auth-type="authType"
|
||||||
|
@close="closeAuthModal"
|
||||||
|
@success="onAuthSuccess"
|
||||||
|
@skip="onAuthSkip"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, defineAsyncComponent } from 'vue'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
@ -169,9 +178,16 @@ const shareIcon = require('@/assets/images/share.svg')
|
||||||
const moreIcon = require('@/assets/images/more.svg')
|
const moreIcon = require('@/assets/images/more.svg')
|
||||||
const roninLogoSrc = 'https://qiniu.drip.im/gh_09bd6126eab8/20250711/upload/4d0efe5313d38f0db01d0cfba4dac9d848bb43a2'
|
const roninLogoSrc = 'https://qiniu.drip.im/gh_09bd6126eab8/20250711/upload/4d0efe5313d38f0db01d0cfba4dac9d848bb43a2'
|
||||||
|
|
||||||
|
// 授权组件
|
||||||
|
const AuthModal = defineAsyncComponent(() => import('../../components/AuthModal/index.vue'))
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const activeTab = ref('highlights')
|
const activeTab = ref('highlights')
|
||||||
|
|
||||||
|
// 授权弹窗状态
|
||||||
|
const authModalVisible = ref(false)
|
||||||
|
const authType = ref<'login' | 'video'>('video')
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
Taro.navigateBack()
|
Taro.navigateBack()
|
||||||
|
@ -192,12 +208,58 @@ const showMore = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 授权相关方法
|
||||||
|
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: '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 = () => {
|
const playMainVideo = () => {
|
||||||
|
if (checkAuthRequired('video')) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '播放主视频',
|
title: '播放主视频',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toggleFullscreen = () => {
|
const toggleFullscreen = () => {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
|
@ -227,11 +289,13 @@ const selectTime = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const playGridVideo = (index: number) => {
|
const playGridVideo = (index: number) => {
|
||||||
|
if (checkAuthRequired('video')) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: `播放视频 ${index + 1}`,
|
title: `播放视频 ${index + 1}`,
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createClip = () => {
|
const createClip = () => {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
|
|
75
client/src/utils/auth.ts
Normal file
75
client/src/utils/auth.ts
Normal 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 '未授权'
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue