新增教练端,实现业务闭环:会员登录注册→查询团课→预约团课→扫码签到→教练端开课→记录实际开课时间→教练端结课→记录实际结课时间→后台查询数据

This commit is contained in:
2026-07-20 17:21:28 +08:00
parent df0e68469b
commit 4a4697c816
84 changed files with 6914 additions and 258 deletions
@@ -50,9 +50,9 @@
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card shadow="hover" class="stat-card stat-card--info">
<div class="stat-card__label">签到总数</div>
<div class="stat-card__value">{{ summary?.signInStatistics?.totalSignIns ?? '-' }}</div>
<el-card shadow="hover" class="stat-card stat-card--danger">
<div class="stat-card__label">教练违规</div>
<div class="stat-card__value">{{ summary?.coachStatistics?.totalViolations ?? '-' }}</div>
</el-card>
</el-col>
</el-row>
@@ -192,12 +192,65 @@
</el-col>
</el-row>
</el-card>
<!-- ========== 教练统计明细 ========== -->
<el-card class="section-card">
<template #header>
<span class="section-title">教练统计</span>
</template>
<el-row :gutter="20">
<el-col :xs="8" :sm="4">
<div class="metric-item">
<span class="metric-label">教练总数</span>
<span class="metric-value">{{ summary?.coachStatistics?.totalCoaches ?? '-' }}</span>
</div>
</el-col>
<el-col :xs="8" :sm="4">
<div class="metric-item">
<span class="metric-label">违规教练</span>
<span class="metric-value" :class="{ 'metric-value--danger': (summary?.coachStatistics?.violatedCoaches ?? 0) > 0 }">
{{ summary?.coachStatistics?.violatedCoaches ?? '-' }}
</span>
</div>
</el-col>
<el-col :xs="8" :sm="4">
<div class="metric-item">
<span class="metric-label">开课总数</span>
<span class="metric-value">{{ summary?.coachStatistics?.totalCourses ?? '-' }}</span>
</div>
</el-col>
<el-col :xs="8" :sm="4">
<div class="metric-item">
<span class="metric-label">迟到</span>
<span class="metric-value" :class="{ 'metric-value--warn': (summary?.coachStatistics?.lateCount ?? 0) > 0 }">
{{ summary?.coachStatistics?.lateCount ?? '-' }}
</span>
</div>
</el-col>
<el-col :xs="8" :sm="4">
<div class="metric-item">
<span class="metric-label">缺席</span>
<span class="metric-value" :class="{ 'metric-value--danger': (summary?.coachStatistics?.absentCount ?? 0) > 0 }">
{{ summary?.coachStatistics?.absentCount ?? '-' }}
</span>
</div>
</el-col>
<el-col :xs="8" :sm="4">
<div class="metric-item">
<span class="metric-label">未手动结课</span>
<span class="metric-value" :class="{ 'metric-value--warn': (summary?.coachStatistics?.notManualEndCount ?? 0) > 0 }">
{{ summary?.coachStatistics?.notManualEndCount ?? '-' }}
</span>
</div>
</el-col>
</el-row>
</el-card>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { Download } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { statisticsApi, type StatisticsSummary } from '@/api/statistics.api'
@@ -278,8 +331,20 @@ const handleExport = async () => {
}
}
let statsTimer: ReturnType<typeof setInterval> | null = null
onMounted(() => {
fetchSummary()
statsTimer = setInterval(() => {
fetchSummary()
}, 60_000)
})
onUnmounted(() => {
if (statsTimer !== null) {
clearInterval(statsTimer)
statsTimer = null
}
})
</script>
@@ -350,6 +415,7 @@ onMounted(() => {
.stat-card--success .stat-card__value { color: #67c23a; }
.stat-card--warning .stat-card__value { color: #e6a23c; }
.stat-card--info .stat-card__value { color: #909399; }
.stat-card--danger .stat-card__value { color: #f56c6c; }
/* ---- 明细分区 ---- */
.section-card {
@@ -380,6 +446,14 @@ onMounted(() => {
color: #303133;
}
.metric-value--warn {
color: #e6a23c;
}
.metric-value--danger {
color: #f56c6c;
}
.rate-group {
display: flex;
flex-direction: column;