智慧教务系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

751 lines
20 KiB

<template>
<el-dialog
v-model="visible"
title="课程安排详情"
width="90%"
class="course-arrangement-dialog"
:destroy-on-close="true"
@open="handleDialogOpen"
>
<div class="course-arrangement-container">
<!-- 课程信息区域 -->
<course-info-section
:schedule-info="scheduleInfo"
:loading="loading"
/>
<!-- 学员区域 -->
<student-section
:formal-students="formalStudents"
:waiting-students="waitingStudents"
:formal-empty-seats="formalEmptySeats"
:waiting-empty-seats="waitingEmptySeats"
:loading="loading"
@add-student="handleAddStudent"
@view-student="handleViewStudent"
@remove-student="handleRemoveStudent"
@upgrade-student="handleUpgradeStudent"
/>
</div>
<!-- 学员搜索弹窗 -->
<student-search-modal
v-model:visible="showSearchModal"
:current-slot="currentSlot"
:preset-student="presetStudent"
@confirm="handleConfirmAddStudent"
/>
<!-- 学员操作菜单 -->
<student-action-menu
v-model:visible="showActionMenu"
:student="currentStudent"
:formal-has-empty="formalEmptySeats.length > 0"
@confirm-action="handleConfirmAction"
/>
<!-- 学员详情弹窗 -->
<student-detail-modal ref="studentDetailModalRef" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getScheduleDetail,
removeStudent,
upgradeStudent,
addSchedule,
updateStudentStatus,
restoreStudent
} from '@/app/api/course_schedule'
import CourseInfoSection from './course-info-section.vue'
import StudentSection from './student-section.vue'
import StudentSearchModal from './student-search-modal.vue'
import StudentActionMenu from './student-action-menu.vue'
import StudentDetailModal from './student-detail-modal.vue'
import type {
StudentInfo,
ScheduleInfo,
SlotInfo,
ScheduleDetailResponse,
AddStudentParams,
RemoveStudentParams,
UpgradeStudentParams,
ApiResponse
} from '@/app/types/course-schedule'
// Props 定义
interface Props {
scheduleId?: number
resourceId?: number
studentId?: number
}
const props = withDefaults(defineProps<Props>(), {
scheduleId: 0,
resourceId: 0,
studentId: 0
})
// 响应式数据
const visible = ref(false)
const loading = ref(false)
const showSearchModal = ref(false)
const showActionMenu = ref(false)
// 课程安排数据
const scheduleInfo = ref<ScheduleInfo>({} as ScheduleInfo)
const formalStudents = ref<StudentInfo[]>([])
const waitingStudents = ref<StudentInfo[]>([])
const formalEmptySeats = ref<number[]>([])
const waitingEmptySeats = ref<number[]>([])
// 操作相关数据
const currentSlot = ref<SlotInfo>({ type: 'formal', index: 1 })
const currentStudent = ref<StudentInfo | null>(null)
const presetStudent = ref<StudentInfo | null>(null)
// 学员详情弹窗引用
const studentDetailModalRef = ref()
// 暴露给父组件的方法
const open = (scheduleId: number, resourceId?: number, studentId?: number) => {
if (scheduleId) {
scheduleInfo.value.id = scheduleId
}
if (resourceId) {
presetStudent.value = {
id: resourceId,
resource_id: resourceId,
student_id: studentId
} as StudentInfo
}
visible.value = true
}
// 对话框打开时的处理
const handleDialogOpen = async () => {
if (scheduleInfo.value.id) {
await loadScheduleDetail()
}
}
// 加载课程安排详情
const loadScheduleDetail = async () => {
try {
loading.value = true
if (!scheduleInfo.value.id) {
ElMessage.warning('课程安排ID不能为空')
return
}
const response: ApiResponse<ScheduleDetailResponse> = await getScheduleDetail({
schedule_id: scheduleInfo.value.id
})
if (response.code === 1 && response.data) {
// 处理嵌套的data结构
const data = response.data.data || response.data
// 更新课程信息
scheduleInfo.value = {
...scheduleInfo.value,
...data.schedule_info
}
// 更新学员数据
formalStudents.value = data.formal_students || []
waitingStudents.value = data.waiting_students || []
// 计算空位数组
formalEmptySeats.value = data.formal_empty_seats || []
waitingEmptySeats.value = data.waiting_empty_seats || []
console.log('课程安排详情加载成功:', {
scheduleInfo: scheduleInfo.value,
formalCount: formalStudents.value.length,
waitingCount: waitingStudents.value.length,
formalEmpty: formalEmptySeats.value.length,
waitingEmpty: waitingEmptySeats.value.length
})
// 成功反馈
nextTick(() => {
ElMessage.success({
message: '课程详情加载成功',
duration: 1500,
showClose: false
})
})
} else {
const errorMsg = response.msg || '获取课程安排详情失败'
ElMessage.error({
message: errorMsg,
duration: 3000,
showClose: true
})
console.error('加载课程详情失败:', response)
}
} catch (error) {
console.error('加载课程安排详情失败:', error)
let errorMessage = '网络异常,获取课程详情失败'
if (error instanceof Error) {
if (error.message.includes('timeout')) {
errorMessage = '请求超时,请检查网络连接'
} else if (error.message.includes('Network')) {
errorMessage = '网络连接失败,请检查网络设置'
}
}
ElMessage.error({
message: errorMessage,
duration: 5000,
showClose: true
})
} finally {
loading.value = false
}
}
// 添加学员
const handleAddStudent = (slotInfo: SlotInfo) => {
currentSlot.value = slotInfo
showSearchModal.value = true
}
// 查看学员详情
const handleViewStudent = (student: StudentInfo) => {
currentStudent.value = student
showActionMenu.value = true
}
// 确认添加学员
const handleConfirmAddStudent = async (student: StudentInfo, slot: SlotInfo) => {
try {
// 数据验证
if (!scheduleInfo.value.id) {
ElMessage.warning('课程安排ID不能为空')
return
}
if (!student.person_type) {
ElMessage.warning('学员类型不能为空')
return
}
const addData: AddStudentParams = {
schedule_id: scheduleInfo.value.id,
person_type: student.person_type,
schedule_type: slot.type === 'formal' ? 1 : 2,
course_date: scheduleInfo.value.course_date || '',
time_slot: scheduleInfo.value.time_slot || ''
}
// 根据学员类型添加对应的ID
if (student.person_type === 'student') {
addData.student_id = student.student_id || student.id
if (!addData.student_id) {
ElMessage.warning('学员ID不能为空')
return
}
} else {
addData.resources_id = student.resources_id || student.resource_id || student.id
if (!addData.resources_id) {
ElMessage.warning('客户资源ID不能为空')
return
}
}
console.log('添加学员数据:', addData)
// 显示加载状态
const loadingMessage = ElMessage({
message: '正在添加学员...',
type: 'info',
duration: 0,
showClose: false
})
const response = await addSchedule(addData)
// 关闭加载状态
loadingMessage.close()
if (response.code === 1) {
ElMessage.success({
message: `成功将学员 ${student.name} 添加到${slot.type === 'formal' ? '正式位' : '等待位'}`,
duration: 3000,
showClose: true
})
await loadScheduleDetail() // 刷新数据
} else {
ElMessage.error({
message: response.msg || '添加学员失败',
duration: 5000,
showClose: true
})
}
} catch (error) {
console.error('添加学员失败:', error)
let errorMessage = '添加学员失败'
if (error instanceof Error) {
if (error.message.includes('timeout')) {
errorMessage = '添加学员超时,请重试'
} else if (error.message.includes('Network')) {
errorMessage = '网络异常,请检查网络连接'
}
}
ElMessage.error({
message: errorMessage,
duration: 5000,
showClose: true
})
}
}
// 确认学员操作
const handleConfirmAction = async (action: string) => {
if (!currentStudent.value) return
try {
switch (action) {
case 'remove':
await handleRemoveStudent(currentStudent.value)
break
case 'upgrade':
await handleUpgradeStudent(currentStudent.value)
break
case 'leave':
await handleStudentLeave(currentStudent.value)
break
case 'cancel_leave':
await handleCancelLeave(currentStudent.value)
break
case 'checkin':
await handleStudentCheckin(currentStudent.value)
break
case 'view':
await handleViewStudentDetails(currentStudent.value)
break
default:
ElMessage.warning('未知操作类型')
break
}
await loadScheduleDetail() // 刷新数据
} catch (error) {
console.error('操作失败:', error)
}
}
// 删除学员
const handleRemoveStudent = async (student: StudentInfo) => {
try {
// 检查学员状态
if (student.status === 1) {
const confirmResult = await ElMessageBox.confirm(
`学员 ${student.name} 已经签到上课,确定要删除吗?\n\n此操作将影响考勤记录,请谨慎操作。`,
'重要提示',
{
confirmButtonText: '仍要删除',
cancelButtonText: '取消',
type: 'error',
showCancelButton: true
}
)
if (confirmResult !== 'confirm') return
}
const { value: reason } = await ElMessageBox.prompt(
`请输入删除学员 ${student.name} 的原因:`,
'删除学员',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
inputPlaceholder: '请输入删除原因(必填)',
inputValue: '',
inputType: 'textarea',
inputValidator: (value) => {
if (!value || value.trim().length === 0) {
return '请输入删除原因'
}
if (value.length > 200) {
return '删除原因不能超过200个字符'
}
return true
},
showCancelButton: true,
closeOnClickModal: false,
closeOnPressEscape: false
}
)
if (reason) {
// 显示加载状态
const loadingMessage = ElMessage({
message: '正在删除学员...',
type: 'info',
duration: 0,
showClose: false
})
const response = await removeStudent({
schedule_id: scheduleInfo.value.id,
person_id: student.person_id || student.id,
person_type: student.person_type,
reason: `管理员删除:${reason.trim()}`
})
loadingMessage.close()
if (response.code === 1) {
ElMessage.success({
message: `学员 ${student.name} 删除成功`,
duration: 3000,
showClose: true
})
} else {
ElMessage.error({
message: response.msg || '删除学员失败',
duration: 5000,
showClose: true
})
}
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除学员失败:', error)
let errorMessage = '删除学员失败'
if (error instanceof Error && error.message.includes('Network')) {
errorMessage = '网络异常,请检查网络连接'
}
ElMessage.error({
message: errorMessage,
duration: 5000,
showClose: true
})
}
}
}
// 升级学员
const handleUpgradeStudent = async (student: StudentInfo) => {
// 验证正式位是否有空位
if (formalEmptySeats.value.length === 0) {
ElMessage.warning({
message: '正式位已满,无法升级。请先移除部分正式位学员或联系管理员扩容。',
duration: 5000,
showClose: true
})
return
}
// 验证学员是否在等待位
if (student.schedule_type !== 2) {
ElMessage.warning('只有等待位学员才能升级为正式位')
return
}
try {
const result = await ElMessageBox.confirm(
`确定要将等待位学员 ${student.name} 升级为正式学员吗?\n\n升级后将占用一个正式位名额,且操作不可撤销。`,
'确认升级',
{
confirmButtonText: '确定升级',
cancelButtonText: '取消',
type: 'warning',
showCancelButton: true,
closeOnClickModal: false,
closeOnPressEscape: false
}
)
if (result === 'confirm') {
// 显示加载状态
const loadingMessage = ElMessage({
message: '正在升级学员...',
type: 'info',
duration: 0,
showClose: false
})
const response = await upgradeStudent({
schedule_id: scheduleInfo.value.id,
person_id: student.person_id || student.id,
from_type: 2, // 从等待位
to_type: 1 // 到正式位
})
loadingMessage.close()
if (response.code === 1) {
ElMessage.success({
message: `学员 ${student.name} 升级成功!已移至正式位`,
duration: 3000,
showClose: true
})
} else {
ElMessage.error({
message: response.msg || '升级失败',
duration: 5000,
showClose: true
})
}
}
} catch (error) {
if (error !== 'cancel') {
console.error('升级学员失败:', error)
let errorMessage = '升级学员失败'
if (error instanceof Error) {
if (error.message.includes('timeout')) {
errorMessage = '升级超时,请重试'
} else if (error.message.includes('Network')) {
errorMessage = '网络异常,请检查网络连接'
}
}
ElMessage.error({
message: errorMessage,
duration: 5000,
showClose: true
})
}
}
}
// 学员请假
const handleStudentLeave = async (student: StudentInfo) => {
try {
// 验证学员状态
if (student.status === 2) {
ElMessage.warning(`学员 ${student.name} 已经在请假状态了`)
return
}
const { value: reason } = await ElMessageBox.prompt(
`请输入学员 ${student.name} 的请假原因:`,
'学员请假',
{
confirmButtonText: '确定请假',
cancelButtonText: '取消',
inputPlaceholder: '请输入请假原因(可选)',
inputValue: '',
inputType: 'textarea',
inputValidator: (value) => {
if (value && value.length > 200) {
return '请假原因不能超过200个字符'
}
return true
},
showCancelButton: true,
closeOnClickModal: false,
closeOnPressEscape: false
}
)
// 显示加载状态
const loadingMessage = ElMessage({
message: '正在处理请假...',
type: 'info',
duration: 0,
showClose: false
})
const response = await removeStudent({
schedule_id: scheduleInfo.value.id,
person_id: student.person_id || student.id,
person_type: student.person_type,
reason: `请假:${reason || '无原因'}`
})
loadingMessage.close()
if (response.code === 1) {
ElMessage.success({
message: `学员 ${student.name} 请假处理成功`,
duration: 3000,
showClose: true
})
} else {
ElMessage.error({
message: response.msg || '请假处理失败',
duration: 5000,
showClose: true
})
}
} catch (error) {
if (error !== 'cancel') {
console.error('学员请假失败:', error)
let errorMessage = '请假处理失败'
if (error instanceof Error && error.message.includes('Network')) {
errorMessage = '网络异常,请检查网络连接'
}
ElMessage.error({
message: errorMessage,
duration: 5000,
showClose: true
})
}
}
}
// 取消请假
const handleCancelLeave = async (student: StudentInfo) => {
try {
const result = await ElMessageBox.confirm(
`确定要取消学员 ${student.name} 的请假状态吗?`,
'取消请假',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}
)
if (result === 'confirm') {
const response = await restoreStudent({
schedule_id: scheduleInfo.value.id,
person_id: student.person_id || student.id
})
if (response.code === 1) {
ElMessage.success('取消请假成功')
} else {
ElMessage.error(response.msg || '取消请假失败')
}
}
} catch (error) {
if (error !== 'cancel') {
console.error('取消请假失败:', error)
ElMessage.error('取消请假失败')
}
}
}
// 学员签到
const handleStudentCheckin = async (student: StudentInfo) => {
try {
const currentStatus = student.status === 1 ? '已签到' : student.status === 2 ? '请假' : '待上课'
// 如果已经签到,询问是否取消签到
if (student.status === 1) {
const result = await ElMessageBox.confirm(
`学员 ${student.name} 当前状态:${currentStatus}\n\n确定要取消签到吗?`,
'取消签到确认',
{
confirmButtonText: '取消签到',
cancelButtonText: '保持现状',
type: 'warning'
}
)
if (result === 'confirm') {
const response = await updateStudentStatus({
schedule_id: scheduleInfo.value.id,
person_id: student.person_id || student.id,
status: 0, // 改为待上课
reason: '管理员取消签到'
})
if (response.code === 1) {
ElMessage.success('已取消签到状态')
} else {
ElMessage.error(response.msg || '取消签到失败')
}
}
} else {
// 其他状态,询问是否签到
const result = await ElMessageBox.confirm(
`学员 ${student.name} 当前状态:${currentStatus}\n\n确定要标记为已签到吗?`,
'签到确认',
{
confirmButtonText: '确认签到',
cancelButtonText: '取消',
type: 'info'
}
)
if (result === 'confirm') {
const response = await updateStudentStatus({
schedule_id: scheduleInfo.value.id,
person_id: student.person_id || student.id,
status: 1, // 标记为已签到
reason: '管理员确认签到'
})
if (response.code === 1) {
ElMessage.success('签到成功')
} else {
ElMessage.error(response.msg || '签到失败')
}
}
}
} catch (error) {
if (error !== 'cancel') {
console.error('更新签到状态失败:', error)
ElMessage.error('更新签到状态失败')
}
}
}
// 查看学员详情
const handleViewStudentDetails = async (student: StudentInfo) => {
try {
// 从StudentInfo中获取student_id或person_id作为学员ID
const studentId = student.student_id || student.person_id || student.id
if (!studentId) {
ElMessage.warning('无法获取学员ID')
return
}
// 打开学员详情弹窗
if (studentDetailModalRef.value) {
await studentDetailModalRef.value.open(studentId)
} else {
ElMessage.error('学员详情组件未正确加载')
}
} catch (error) {
console.error('查看学员详情失败:', error)
ElMessage.error('查看学员详情失败')
}
}
// 暴露给父组件的方法
defineExpose({
open
})
</script>
<style lang="scss" scoped>
.course-arrangement-dialog {
.course-arrangement-container {
display: flex;
flex-direction: column;
gap: 24px;
max-height: 70vh;
overflow-y: auto;
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
padding: 20px 20px 10px 20px;
margin-right: 0;
}
</style>