新增教练端,实现业务闭环:会员登录注册→查询团课→预约团课→扫码签到→教练端开课→记录实际开课时间→教练端结课→记录实际结课时间→后台查询数据
This commit is contained in:
@@ -27,6 +27,21 @@ export interface CoachUpdateRequest {
|
||||
phone?: string
|
||||
}
|
||||
|
||||
export interface ViolationCountItem {
|
||||
coach_id: number
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface ViolationRecord {
|
||||
id: number
|
||||
coach_id: number
|
||||
course_id: number
|
||||
violation_time: string
|
||||
violation_reason: string
|
||||
course_name?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export const coachApi = {
|
||||
/** 获取所有教练列表 */
|
||||
getAll: () =>
|
||||
@@ -47,4 +62,12 @@ export const coachApi = {
|
||||
/** 获取教练教授的团课列表 */
|
||||
getCourses: (id: string) =>
|
||||
request.get<GroupCourse[]>(`/coach/${id}/courses`),
|
||||
|
||||
/** 获取所有教练的违规次数统计 */
|
||||
getViolationCounts: () =>
|
||||
request.get<ViolationCountItem[]>('/coach/violation-counts'),
|
||||
|
||||
/** 获取指定教练的违规记录 */
|
||||
getViolations: (id: string) =>
|
||||
request.get<ViolationRecord[]>(`/coach/${id}/violations`),
|
||||
}
|
||||
|
||||
@@ -36,11 +36,22 @@ export interface SignInStatistics {
|
||||
faceSignIns: number
|
||||
}
|
||||
|
||||
export interface CoachStatistics {
|
||||
totalCoaches: number
|
||||
totalViolations: number
|
||||
lateCount: number
|
||||
absentCount: number
|
||||
notManualEndCount: number
|
||||
violatedCoaches: number
|
||||
totalCourses: number
|
||||
}
|
||||
|
||||
export interface StatisticsSummary {
|
||||
statDate: string
|
||||
memberStatistics: MemberStatistics
|
||||
bookingStatistics: BookingStatistics
|
||||
signInStatistics: SignInStatistics
|
||||
coachStatistics: CoachStatistics
|
||||
generatedAt: string
|
||||
}
|
||||
|
||||
|
||||
@@ -27,11 +27,16 @@
|
||||
<el-option value="1" label="已取消" />
|
||||
<el-option value="2" label="已结束" />
|
||||
<el-option value="3" label="进行中" />
|
||||
<el-option value="4" label="超时" />
|
||||
<el-option value="5" label="教练缺席" />
|
||||
<el-option value="6" label="自动结束" />
|
||||
<el-option value="7" label="教练迟到" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="fetchData" :loading="loading" circle>
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
新增团课
|
||||
@@ -82,12 +87,12 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="startTime" label="开始时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.startTime) }}
|
||||
{{ formatDateTime(row.actualStartTime || row.startTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="endTime" label="结束时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.endTime) }}
|
||||
{{ formatDateTime(row.actualEndTime || row.endTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="location" label="上课地点" width="120" />
|
||||
@@ -96,11 +101,14 @@
|
||||
{{ row.currentMembers || 0 }}/{{ row.maxMembers || 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="info" link size="small" @click="showDetail(row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button type="success" link size="small" @click="showQrCode(row)">
|
||||
二维码
|
||||
</el-button>
|
||||
@@ -267,13 +275,51 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 团课详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="团课详情"
|
||||
width="640px"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="ID">{{ detailRow.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="课程名称">{{ detailRow.courseName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="课程类型">{{ getCourseTypeName(detailRow.courseType) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="教练">{{ getCoachDisplayName(detailRow.coachId) || detailRow.coachName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="课程状态">
|
||||
<el-tag :type="statusMap[detailRow.status || '']?.type || 'info'" effect="dark" size="small">
|
||||
{{ statusMap[detailRow.status || '']?.label || detailRow.status }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上课地点">{{ detailRow.location || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计划开始时间">{{ formatDateTime(detailRow.startTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="实际开课时间">{{ formatDateTime(detailRow.actualStartTime) || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计划结束时间">{{ formatDateTime(detailRow.endTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="实际结课时间">{{ formatDateTime(detailRow.actualEndTime) || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人数">{{ detailRow.currentMembers || 0 }} / {{ detailRow.maxMembers || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="储值卡额度">{{ detailRow.storedValueAmount ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="封面图片">
|
||||
<img v-if="detailRow.coverImage && coverCache[detailRow.coverImage]" :src="coverCache[detailRow.coverImage]" class="detail-cover" />
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="课程描述" :span="2">{{ detailRow.description || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人">{{ detailRow.createBy || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新人">{{ detailRow.updateBy || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDateTime(detailRow.createdAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDateTime(detailRow.updatedAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { ref, reactive, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, WarningFilled } from '@element-plus/icons-vue'
|
||||
import { Search, Plus, WarningFilled, Refresh } from '@element-plus/icons-vue'
|
||||
import { groupCourseApi, groupCourseTypeApi, type GroupCourse, type GroupCourseType } from '@/api/groupCourse.api'
|
||||
import { coachApi, type Coach } from '@/api/coach.api'
|
||||
import request from '@/utils/request'
|
||||
@@ -291,7 +337,9 @@ const statusMap: Record<string, { type: string; label: string }> = {
|
||||
'1': { type: 'danger', label: '已取消' },
|
||||
'2': { type: 'info', label: '已结束' },
|
||||
'3': { type: '', label: '进行中' },
|
||||
'4': { type: 'warning', label: '超时' },
|
||||
'5': { type: 'danger', label: '教练缺席' },
|
||||
'6': { type: 'warning', label: '自动结束' },
|
||||
'7': { type: 'warning', label: '教练迟到' },
|
||||
}
|
||||
|
||||
const pagination = reactive({
|
||||
@@ -340,6 +388,18 @@ const formRules = {
|
||||
|
||||
const coverCache = reactive<Record<string, string>>({})
|
||||
|
||||
// ===== 详情弹窗 =====
|
||||
const detailVisible = ref(false)
|
||||
const detailRow = ref<GroupCourse>({} as GroupCourse)
|
||||
|
||||
const showDetail = (row: GroupCourse) => {
|
||||
detailRow.value = row
|
||||
detailVisible.value = true
|
||||
if (row.coverImage) {
|
||||
loadRowCover(row.coverImage)
|
||||
}
|
||||
}
|
||||
|
||||
const loadRowCover = async (coverImage: string | undefined) => {
|
||||
if (!coverImage) return
|
||||
// already loaded
|
||||
@@ -738,9 +798,22 @@ const handleModalOk = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([fetchCourseTypes(), fetchCoaches()])
|
||||
fetchData()
|
||||
// 每30秒自动刷新列表,捕获调度器状态变更
|
||||
refreshTimer = setInterval(() => {
|
||||
fetchData()
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -837,4 +910,11 @@ onMounted(async () => {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-cover {
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="filteredData" style="width: 100%">
|
||||
<el-table v-loading="loading" :data="filteredData" style="width: 100%" :row-class-name="getRowClass">
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="nickname" label="昵称" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
@@ -41,12 +41,19 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="违规次数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getViolationTag(Number(row.id))" effect="dark" round>
|
||||
{{ violationCounts[Number(row.id)] ?? 0 }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300">
|
||||
<el-table-column label="操作" width="380">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
编辑
|
||||
@@ -54,6 +61,9 @@
|
||||
<el-button type="info" link @click="handleViewCourses(row)">
|
||||
查看团课
|
||||
</el-button>
|
||||
<el-button type="success" link @click="handleViewDetail(row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
type="danger"
|
||||
@@ -138,6 +148,88 @@
|
||||
<el-button @click="coursesDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 教练详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
:title="'教练详情 - ' + (detailCoach?.nickname || detailCoach?.username || '')"
|
||||
width="960px"
|
||||
destroy-on-close
|
||||
>
|
||||
<template v-if="detailCoach">
|
||||
<!-- 基本信息 -->
|
||||
<el-descriptions :column="3" border size="small" style="margin-bottom: 20px">
|
||||
<el-descriptions-item label="用户名">{{ detailCoach.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="昵称">{{ detailCoach.nickname }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="detailCoach.status === 1 ? 'success' : 'danger'" effect="dark">
|
||||
{{ detailCoach.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ detailCoach.email }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ detailCoach.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="教练ID">{{ detailCoach.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="违规次数">
|
||||
<el-tag :type="getViolationTag(Number(detailCoach.id))" effect="dark" round>
|
||||
{{ violationCounts[Number(detailCoach.id)] ?? 0 }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDateTime(detailCoach.createdAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDateTime(detailCoach.updatedAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 违规记录 -->
|
||||
<h4 style="margin-bottom: 10px; color: #E6A23C;">违规记录</h4>
|
||||
<el-table v-loading="detailViolationsLoading" :data="detailViolations" style="width: 100%" max-height="240" empty-text="暂无违规记录">
|
||||
<el-table-column prop="course_name" label="课程名称" min-width="120" />
|
||||
<el-table-column prop="course_id" label="课程ID" width="80" />
|
||||
<el-table-column label="违规类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getViolationTypeTag(row.violation_reason)" effect="dark">
|
||||
{{ getViolationTypeText(row.violation_reason) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="违规时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.violation_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 团课记录 -->
|
||||
<h4 style="margin-top: 20px; margin-bottom: 10px; color: #409EFF;">团课记录</h4>
|
||||
<el-table v-loading="detailCoursesLoading" :data="detailCourses" style="width: 100%" max-height="240" empty-text="暂无团课记录">
|
||||
<el-table-column prop="courseName" label="课程名称" min-width="120" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getCourseStatusTag(row.status)" effect="dark">
|
||||
{{ getCourseStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开课时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.startTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结课时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.endTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约人数" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.currentMembers }} / {{ row.maxMembers }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="location" label="上课地点" />
|
||||
</el-table>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -145,13 +237,14 @@
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { coachApi, type Coach, type CoachCreateRequest, type CoachUpdateRequest, type GroupCourse } from '@/api/coach.api'
|
||||
import { coachApi, type Coach, type CoachCreateRequest, type CoachUpdateRequest, type GroupCourse, type ViolationCountItem, type ViolationRecord } from '@/api/coach.api'
|
||||
import { handleApiError } from '@/utils/errorHandler'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<Coach[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const violationCounts = reactive<Record<number, number>>({})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
if (!searchKeyword.value) return dataSource.value
|
||||
@@ -209,7 +302,24 @@ const courseStatusMap: Record<number, { text: string; tag: string }> = {
|
||||
1: { text: '已取消', tag: 'info' },
|
||||
2: { text: '已结束', tag: 'warning' },
|
||||
3: { text: '进行中', tag: 'danger' },
|
||||
4: { text: '超时', tag: 'info' },
|
||||
5: { text: '教练缺席', tag: 'danger' },
|
||||
6: { text: '自动结束', tag: 'info' },
|
||||
7: { text: '教练迟到', tag: 'warning' },
|
||||
}
|
||||
|
||||
// 违规类型映射
|
||||
const violationTypeMap: Record<string, { text: string; tag: string }> = {
|
||||
COACH_LATE: { text: '教练迟到', tag: 'warning' },
|
||||
COACH_ABSENT: { text: '教练缺席', tag: 'danger' },
|
||||
NOT_MANUAL_END: { text: '未手动结课', tag: 'info' },
|
||||
}
|
||||
|
||||
function getViolationTypeText(reason: string) {
|
||||
return violationTypeMap[reason]?.text || reason
|
||||
}
|
||||
|
||||
function getViolationTypeTag(reason: string) {
|
||||
return violationTypeMap[reason]?.tag || 'info'
|
||||
}
|
||||
|
||||
function getCourseStatusText(status: number) {
|
||||
@@ -224,6 +334,13 @@ const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
dataSource.value = await coachApi.getAll()
|
||||
// 同步获取违规次数统计
|
||||
const counts = await coachApi.getViolationCounts()
|
||||
// 清空旧数据
|
||||
Object.keys(violationCounts).forEach(key => delete violationCounts[Number(key)])
|
||||
counts.forEach(item => {
|
||||
violationCounts[item.coach_id] = item.count
|
||||
})
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
} finally {
|
||||
@@ -333,6 +450,49 @@ const handleViewCourses = async (row: Coach) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 违规次数标签颜色
|
||||
function getViolationTag(coachId: number) {
|
||||
const count = violationCounts[coachId] ?? 0
|
||||
if (count === 0) return 'success'
|
||||
if (count <= 2) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
// 违规行高亮
|
||||
function getRowClass({ row }: { row: Coach }) {
|
||||
const count = violationCounts[Number(row.id)] ?? 0
|
||||
return count > 0 ? 'row--has-violation' : ''
|
||||
}
|
||||
|
||||
// 详情弹窗
|
||||
const detailDialogVisible = ref(false)
|
||||
const detailCoach = ref<Coach | null>(null)
|
||||
const detailViolations = ref<ViolationRecord[]>([])
|
||||
const detailViolationsLoading = ref(false)
|
||||
const detailCourses = ref<GroupCourse[]>([])
|
||||
const detailCoursesLoading = ref(false)
|
||||
|
||||
const handleViewDetail = async (row: Coach) => {
|
||||
detailCoach.value = row
|
||||
detailDialogVisible.value = true
|
||||
// 并行加载违规记录和团课记录
|
||||
detailViolationsLoading.value = true
|
||||
detailCoursesLoading.value = true
|
||||
try {
|
||||
const [violations, courses] = await Promise.all([
|
||||
coachApi.getViolations(row.id),
|
||||
coachApi.getCourses(row.id),
|
||||
])
|
||||
detailViolations.value = violations
|
||||
detailCourses.value = courses
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
} finally {
|
||||
detailViolationsLoading.value = false
|
||||
detailCoursesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
@@ -353,4 +513,10 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 有违规记录的教练行:左侧红色边框 + 浅色背景 */
|
||||
:deep(.row--has-violation) {
|
||||
border-left: 3px solid #F56C6C;
|
||||
background-color: #fef0f0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user