新增教练管理,优化预约人数计数方式

This commit is contained in:
2026-07-19 19:02:10 +08:00
parent 97a5eff678
commit df0e68469b
26 changed files with 1537 additions and 264 deletions
@@ -64,6 +64,11 @@
{{ getCourseTypeName(row.courseType) }}
</template>
</el-table-column>
<el-table-column label="教练" width="100">
<template #default="{ row }">
{{ getCoachDisplayName(row.coachId) || row.coachName || '-' }}
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag
@@ -150,6 +155,28 @@
/>
</el-select>
</el-form-item>
<el-form-item label="教练" prop="coachId">
<el-select
v-model="formState.coachId"
placeholder="请选择教练"
clearable
style="width: 100%"
>
<el-option
v-for="coach in coaches"
:key="coach.id"
:value="coach.id"
:label="`${coach.nickname || coach.username} (${coach.username})`"
/>
</el-select>
<div v-if="coachConflictWarning" style="margin-top: 6px; color: #e6a23c; font-size: 12px; line-height: 1.5;">
<el-icon style="vertical-align: middle;"><WarningFilled /></el-icon>
{{ coachConflictWarning }}
</div>
<div v-if="coachConflictChecking" style="margin-top: 4px; color: #909399; font-size: 12px;">
正在检查教练排期...
</div>
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="formState.startTime"
@@ -246,8 +273,9 @@
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus } from '@element-plus/icons-vue'
import { Search, Plus, WarningFilled } 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'
import { formatDateTime } from '@/utils/dateFormat'
@@ -256,6 +284,7 @@ const dataSource = ref<GroupCourse[]>([])
const searchKeyword = ref('')
const searchStatus = ref('')
const courseTypes = ref<GroupCourseType[]>([])
const coaches = ref<Coach[]>([])
const statusMap: Record<string, { type: string; label: string }> = {
'0': { type: 'success', label: '正常' },
@@ -279,9 +308,12 @@ const sortInfo = reactive({
const modalVisible = ref(false)
const modalTitle = ref('')
const formRef = ref()
const coachConflictWarning = ref('')
const coachConflictChecking = ref(false)
const formState = reactive<GroupCourse>({
courseName: '',
courseType: undefined,
coachId: undefined,
startTime: '',
endTime: '',
maxMembers: 20,
@@ -342,12 +374,70 @@ const fetchCourseTypes = async () => {
}
}
const fetchCoaches = async () => {
try {
coaches.value = await coachApi.getAll()
} catch {
// ignore
}
}
const getCourseTypeName = (typeId: number | undefined): string => {
if (typeId === undefined || typeId === null) return '-'
const type = courseTypes.value.find(t => t.id === typeId)
return type?.typeName || String(typeId)
}
const getCoachDisplayName = (coachId: number | string | undefined): string => {
if (coachId === undefined || coachId === null) return ''
const coach = coaches.value.find(c => String(c.id) === String(coachId))
if (!coach) return ''
return coach.nickname || coach.username
}
const getComputedEndTime = (): string => {
if (!formState.startTime || !(formState as any).duration) return ''
const start = new Date(formState.startTime)
if (isNaN(start.getTime())) return ''
start.setMinutes(start.getMinutes() + (formState as any).duration)
const pad = (n: number) => String(n).padStart(2, '0')
return `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}T${pad(start.getHours())}:${pad(start.getMinutes())}:${pad(start.getSeconds())}`
}
const checkCoachConflict = async () => {
const coachId = formState.coachId
const startTime = formState.startTime
const durationVal = (formState as any).duration
if (!coachId || !startTime || !durationVal) {
coachConflictWarning.value = ''
return
}
const endTime = getComputedEndTime()
if (!endTime) {
coachConflictWarning.value = ''
return
}
coachConflictChecking.value = true
try {
const res = await groupCourseApi.checkCoachConflict({
coachId,
startTime,
endTime,
excludeCourseId: formState.id,
})
if ((res as any).hasConflict && (res as any).conflicts?.length > 0) {
const names = (res as any).conflicts.map((c: any) => c.courseName || '未知课程').join('、')
coachConflictWarning.value = `该教练在此时间段已有团课:${names}`
} else {
coachConflictWarning.value = ''
}
} catch {
coachConflictWarning.value = ''
} finally {
coachConflictChecking.value = false
}
}
const fetchData = async () => {
loading.value = true
try {
@@ -522,12 +612,25 @@ const loadCoverPreview = async (val: string) => {
// Watch coverImage changes to auto-load preview (e.g. when editing an existing course)
watch(() => formState.coverImage, (val) => loadCoverPreview(val || ''), { immediate: true })
// 监听教练选择和时间变化,检查冲突
watch(() => [formState.coachId, formState.startTime, (formState as any).duration] as const, () => {
checkCoachConflict()
}, { deep: false })
// 打开弹窗时清除冲突提示
watch(modalVisible, (val) => {
if (val) {
coachConflictWarning.value = ''
}
})
const handleAdd = () => {
modalTitle.value = '新增团课'
Object.assign(formState, {
id: undefined,
courseName: '',
courseType: undefined,
coachId: undefined,
startTime: '',
endTime: '',
maxMembers: 20,
@@ -591,12 +694,35 @@ const handleModalOk = async () => {
try {
await formRef.value.validate()
// Auto-calculate endTime from startTime + duration
if (formState.startTime && formState.duration) {
if (formState.startTime && (formState as any).duration) {
const start = new Date(formState.startTime)
start.setMinutes(start.getMinutes() + formState.duration)
start.setMinutes(start.getMinutes() + (formState as any).duration)
const pad = (n: number) => String(n).padStart(2, '0')
formState.endTime = `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}T${pad(start.getHours())}:${pad(start.getMinutes())}:${pad(start.getSeconds())}`
}
// 最终教练冲突检查
if (formState.coachId && formState.startTime && formState.endTime) {
try {
const res = await groupCourseApi.checkCoachConflict({
coachId: formState.coachId,
startTime: formState.startTime,
endTime: formState.endTime,
excludeCourseId: formState.id,
})
if ((res as any).hasConflict && (res as any).conflicts?.length > 0) {
const names = (res as any).conflicts.map((c: any) => c.courseName || '未知课程').join('、')
await ElMessageBox.confirm(
`该教练在此时间段已有团课:${names},确定继续吗?`,
'时间冲突警告',
{ confirmButtonText: '继续保存', cancelButtonText: '取消', type: 'warning' }
)
// 用户点击了"继续保存",继续执行
}
} catch (e) {
// 用户取消了冲突确认对话框,中止保存
return
}
}
if (formState.id) {
await groupCourseApi.update(formState.id, formState)
} else {
@@ -612,8 +738,8 @@ const handleModalOk = async () => {
}
}
onMounted(() => {
fetchCourseTypes()
onMounted(async () => {
await Promise.all([fetchCourseTypes(), fetchCoaches()])
fetchData()
})
</script>