新增教练管理,优化预约人数计数方式
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user