450 lines
10 KiB
Vue
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">🏋</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>
|