新增教练端,实现业务闭环:会员登录注册→查询团课→预约团课→扫码签到→教练端开课→记录实际开课时间→教练端结课→记录实际结课时间→后台查询数据
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user