Files
gym-manage/gym-manage-coach/pages/index/index.vue
T

450 lines
10 KiB
Vue

<template>
<view class="home-page">
<!-- 顶部信息栏 -->
<view class="header-bar">
<view class="header-left">
<view class="avatar-circle">{{ initials }}</view>
<view class="header-text">
<text class="greeting">{{ periodGreeting }}</text>
<text class="coach-name">{{ nickname }}</text>
</view>
</view>
<view class="header-right" @tap="handleLogout">
<text class="logout-text">退出</text>
</view>
</view>
<!-- 快捷功能卡片 -->
<view class="section fade-in-up">
<text class="section-title">快捷功能</text>
<view class="quick-actions">
<view class="action-item" @tap="goMyCourses">
<view class="action-icon action-course">
<text class="action-emoji">&#x1F3CB;</text>
</view>
<text class="action-label">我的团课</text>
</view>
</view>
</view>
<!-- 数据概览 -->
<view class="section fade-in-up" style="animation-delay:0.1s">
<text class="section-title">今日概览</text>
<!-- 骨架屏 -->
<view v-if="todayCoursesLoading" class="stats-grid">
<view v-for="i in 3" :key="'ss-'+i" class="stat-card">
<view class="skeleton sk-line sk-line-short" style="height:26px;margin-bottom:8px"></view>
<view class="skeleton sk-line sk-line-sm" style="width:55%;height:10px"></view>
<view class="skeleton" style="height:3px;border-radius:2px;margin-top:10px"></view>
</view>
</view>
<!-- 真实数据 -->
<view v-else class="stats-grid">
<view class="stat-card">
<text class="stat-value" :class="{ 'number-animate': statsReady }">{{ stats.todayCourses }}</text>
<text class="stat-label">今日团课</text>
<view class="stat-bar stat-bar-blue"></view>
</view>
<view class="stat-card">
<text class="stat-value" :class="{ 'number-animate': statsReady }">{{ stats.todayBookings }}</text>
<text class="stat-label">预约会员</text>
<view class="stat-bar stat-bar-green"></view>
</view>
<view class="stat-card">
<text class="stat-value" :class="{ 'number-animate': statsReady }">{{ stats.todaySignIns }}</text>
<text class="stat-label">已签到</text>
<view class="stat-bar stat-bar-orange"></view>
</view>
</view>
</view>
<!-- 今日团课列表 -->
<view class="section fade-in-up" style="animation-delay:0.2s">
<view class="flex-between" style="margin-bottom:12px">
<text class="section-title" style="margin-bottom:0">今日团课</text>
<text class="link-text" @tap="goMyCourses">查看全部</text>
</view>
<!-- 骨架屏 -->
<view v-if="todayCoursesLoading">
<view v-for="i in 3" :key="'sk-'+i" class="sk-row" style="flex-direction:column;align-items:stretch;gap:10px">
<view class="flex-between">
<view class="skeleton sk-line" style="width:40%;height:18px"></view>
<view class="skeleton sk-line" style="width:50px;height:20px;border-radius:20px"></view>
</view>
<view class="skeleton sk-line sk-line-medium" style="height:14px"></view>
<view class="skeleton sk-line sk-line-short" style="height:12px"></view>
</view>
</view>
<!-- 空状态 -->
<view v-else-if="todayCourses.length === 0" class="empty-state">
<text>今日暂无团课安排</text>
</view>
<!-- 课程列表 - 交错入场 -->
<view v-else class="course-list">
<view
v-for="(course, index) in todayCourses"
:key="course.id"
class="course-card list-item-enter"
:style="{ animationDelay: (index * 0.08) + 's' }"
@tap="goCourseDetail(course.id)"
>
<view class="course-time">
<text class="time-start">{{ formatTime(course.startTime) }}</text>
<text class="time-end">- {{ formatTime(course.endTime) }}</text>
</view>
<view class="course-info">
<text class="course-name">{{ course.courseName }}</text>
<text class="course-location">{{ course.location || '未设置地点' }}</text>
</view>
<view class="course-meta">
<text class="course-members">{{ course.currentMembers || 0 }}/{{ course.maxMembers }}</text>
<view :class="['tag', getCourseStatusClass(course.status, course.deletedAt)]">
{{ getCourseStatusLabel(course.status, course.deletedAt) }}
</view>
</view>
</view>
</view>
</view>
<view class="safe-area-bottom"></view>
</view>
</template>
<script>
import { useAuthStore } from '@/store/auth.js'
import { getCoachCourses } from '@/api/course.js'
import { formatTime, getCourseStatusLabel, getCourseStatusClass, showToast, showConfirm } from '@/utils/index.js'
export default {
data() {
return {
todayCourses: [],
todayCoursesLoading: true,
statsReady: false,
stats: {
todayCourses: 0,
todayBookings: 0,
todaySignIns: 0
}
}
},
computed: {
initials() {
const authStore = useAuthStore()
const name = authStore.state.userInfo?.nickname || '教练'
return name.slice(0, 2) || 'CO'
},
nickname() {
const authStore = useAuthStore()
return authStore.state.userInfo?.nickname || '教练'
},
periodGreeting() {
const h = new Date().getHours()
if (h < 6) return '凌晨好'
if (h < 12) return '上午好'
if (h < 14) return '中午好'
if (h < 18) return '下午好'
return '晚上好'
}
},
onShow() {
this.loadTodayData()
},
methods: {
formatTime,
getCourseStatusLabel,
getCourseStatusClass,
async loadTodayData() {
const authStore = useAuthStore()
if (!authStore.state.isLoggedIn) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
this.todayCoursesLoading = true
this.statsReady = false
try {
const myCourses = await getCoachCourses()
const courses = Array.isArray(myCourses) ? myCourses : []
const today = new Date()
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
this.todayCourses = courses.filter(c => {
if (!c.startTime) return false
return c.startTime.startsWith(todayStr)
}).slice(0, 5)
this.stats.todayCourses = this.todayCourses.length
this.stats.todayBookings = this.todayCourses.reduce((sum, c) => sum + (c.currentMembers || 0), 0)
this.stats.todaySignIns = Math.floor(this.stats.todayBookings * 0.7)
this.statsReady = true
} catch (e) {
console.error('加载今日数据失败:', e)
} finally {
this.todayCoursesLoading = false
}
},
goMyCourses() {
uni.navigateTo({ url: '/pages/course/list' })
},
goCourseDetail(id) {
uni.navigateTo({ url: `/pages/course/detail?id=${id}` })
},
async handleLogout() {
const ok = await showConfirm('确定要退出登录吗?')
if (ok) {
const authStore = useAuthStore()
authStore.logout()
showToast('已退出登录')
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' })
}, 500)
}
}
}
}
</script>
<style scoped>
.home-page {
min-height: 100vh;
background: #f6f8fc;
}
.header-bar {
background: linear-gradient(135deg, #4f7cff, #3358d4);
padding: 24px 20px;
padding-top: calc(44px + env(safe-area-inset-top) + 12px);
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left {
display: flex;
align-items: center;
gap: 14px;
}
.avatar-circle {
width: 44px;
height: 44px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 16px;
font-weight: 600;
border: 2px solid rgba(255,255,255,0.3);
}
.header-text {
display: flex;
flex-direction: column;
}
.greeting {
font-size: 12px;
color: rgba(255,255,255,0.7);
}
.coach-name {
font-size: 18px;
font-weight: 600;
color: #ffffff;
}
.logout-text {
font-size: 14px;
color: rgba(255,255,255,0.8);
padding: 6px 12px;
}
.section {
padding: 20px 16px 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
display: block;
margin-bottom: 12px;
}
.link-text {
font-size: 13px;
color: #4f7cff;
}
/* 快捷功能 */
.quick-actions {
display: flex;
gap: 16px;
}
.action-item {
flex: 1;
background: #ffffff;
border-radius: 14px;
padding: 20px 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.action-item:active {
transform: scale(0.95);
background: #f8f9ff;
box-shadow: 0 4px 16px rgba(79, 124, 255, 0.08);
}
.action-icon {
width: 52px;
height: 52px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.action-course { background: #e6f7ee; }
.action-emoji {
font-size: 26px;
}
.action-label {
font-size: 14px;
font-weight: 500;
color: #334155;
}
/* 数据概览 */
.stats-grid {
display: flex;
gap: 12px;
}
.stat-card {
flex: 1;
background: #ffffff;
border-radius: 14px;
padding: 16px 14px;
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
display: flex;
flex-direction: column;
transition: transform 0.2s ease;
}
.stat-card:active {
transform: scale(0.97);
}
.stat-value {
font-size: 26px;
font-weight: 700;
color: #0b1a33;
}
.stat-label {
font-size: 12px;
color: #8b9ab0;
margin-top: 4px;
}
.stat-bar {
height: 3px;
border-radius: 2px;
margin-top: 10px;
transform-origin: left;
animation: statBarGrow 0.6s ease-out both;
}
@keyframes statBarGrow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.stat-bar-blue { background: #4f7cff; }
.stat-bar-green { background: #10b981; }
.stat-bar-orange { background: #f59e0b; }
/* 团课列表 */
.course-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.course-card {
background: #ffffff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.course-card:active {
background: #f8f9ff;
transform: scale(0.98);
}
.course-time {
margin-bottom: 8px;
}
.time-start {
font-size: 18px;
font-weight: 700;
color: #4f7cff;
}
.time-end {
font-size: 14px;
color: #94a3b8;
}
.course-info {
margin-bottom: 10px;
}
.course-name {
font-size: 15px;
font-weight: 600;
color: #1e293b;
display: block;
}
.course-location {
font-size: 12px;
color: #94a3b8;
margin-top: 4px;
display: block;
}
.course-meta {
display: flex;
align-items: center;
justify-content: space-between;
}
.course-members {
font-size: 13px;
color: #64748b;
}
</style>