2025-12-26 08:15:02 +00:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="index-lists">
|
|
|
|
|
|
<el-card class="!border-none" shadow="never">
|
|
|
|
|
|
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
|
|
|
|
|
<el-form-item label="班级" prop="classId">
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="queryParams.classId"
|
|
|
|
|
|
placeholder="请输入班级名称搜索"
|
|
|
|
|
|
:remote="true"
|
|
|
|
|
|
:remote-method="handleClassRemoteSearch"
|
|
|
|
|
|
:loading="classSearchLoading"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
clearable
|
|
|
|
|
|
remote-show-suffix
|
|
|
|
|
|
class="w-[280px]"
|
|
|
|
|
|
@change="handleClassChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="item in getClassLists.lists"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
:value="item.id"
|
|
|
|
|
|
:label="item.className"
|
|
|
|
|
|
@click="handleStudentClick(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default>
|
|
|
|
|
|
<span
|
|
|
|
|
|
style="color: var(--el-text-color-secondary); font-size: 13px"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.classCode }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span style="margin-left: 8px">{{ item.className }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="学期" prop="semesterId">
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="queryParams.semesterId"
|
|
|
|
|
|
class="w-[280px]"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@change="handleSemesterChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="college in getConfigLists.lists"
|
|
|
|
|
|
:key="college.id"
|
|
|
|
|
|
:value="college.id"
|
|
|
|
|
|
:label="college.config"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
2025-12-29 06:15:08 +00:00
|
|
|
|
<!-- 选中时间段信息显示和清除按钮 -->
|
|
|
|
|
|
<el-form-item v-if="selectedTimeSlot" label="已选中时间段">
|
|
|
|
|
|
<el-tag type="info" size="large" closable @close="clearSelectedTimeSlot">
|
|
|
|
|
|
{{ getSelectedTimeSlotText() }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</el-form-item>
|
2025-12-26 08:15:02 +00:00
|
|
|
|
</el-form>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
<div class="flex">
|
|
|
|
|
|
<el-card class="!border-none mt-4 mr-4" shadow="never" style="width: 75%">
|
|
|
|
|
|
<el-table class="mb-4" size="large" v-loading="loading" :data="tableData" border>
|
|
|
|
|
|
<!-- 序号列(包含时间、类型、状态) -->
|
|
|
|
|
|
<el-table-column label="节次" width="50" class-name="first-column" align="left">
|
|
|
|
|
|
<template #default="{ $index }">
|
|
|
|
|
|
<div class="index-info">
|
|
|
|
|
|
<div class="index-details">
|
|
|
|
|
|
<div class="index-number">{{ $index + 1 }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="时间" width="80" class-name="first-column" align="left">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="index-info">
|
|
|
|
|
|
<div class="index-details">
|
|
|
|
|
|
<div class="flex justify-center" style="font-size: 12px">
|
|
|
|
|
|
{{ getRowStartTime(row) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex justify-center" style="font-size: 6px">——</div>
|
|
|
|
|
|
<div class="flex justify-center" style="font-size: 12px">
|
|
|
|
|
|
{{ getRowEndTime(row) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-12-29 06:15:08 +00:00
|
|
|
|
<!-- 星期列改为循环渲染,方便处理点击事件 -->
|
|
|
|
|
|
<el-table-column
|
|
|
|
|
|
v-for="day in weekDays"
|
|
|
|
|
|
:key="day.value"
|
|
|
|
|
|
:label="day.label"
|
|
|
|
|
|
min-width="80"
|
|
|
|
|
|
align="center"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default="{ row, $index }">
|
|
|
|
|
|
<div
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'time-slot-cell',
|
|
|
|
|
|
isTimeSlotSelected($index, day.value, row[day.value]) ? 'time-slot-selected' : '',
|
|
|
|
|
|
row[day.value] ? '' : 'empty-slot'
|
|
|
|
|
|
]"
|
|
|
|
|
|
@click="handleTimeSlotClick($index, day.value, row[day.value])"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 如果有课程信息,显示课程 -->
|
|
|
|
|
|
<div v-if="row[day.value]" class="course-info">
|
|
|
|
|
|
<div class="course-name">{{ row[day.value].courseName || '课程' }}</div>
|
|
|
|
|
|
<div class="course-details">
|
|
|
|
|
|
<div>教师: {{ row[day.value].teacherName || '未知' }}</div>
|
|
|
|
|
|
<div>教室: {{ row[day.value].classroomName || '未知' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 空时间段显示可选状态 -->
|
|
|
|
|
|
<div v-else class="empty-slot-content">
|
|
|
|
|
|
<el-icon v-if="isTimeSlotSelected($index, day.value, row[day.value])" class="check-icon">
|
|
|
|
|
|
<Check />
|
|
|
|
|
|
</el-icon>
|
|
|
|
|
|
<span v-else class="empty-text">点击选择</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-26 08:15:02 +00:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
<el-card class="mt-4" shadow="never" style="width: 24%">
|
|
|
|
|
|
<!-- 改为标签页 -->
|
|
|
|
|
|
<el-tabs v-model="activeTab" style="margin-top: -10px">
|
|
|
|
|
|
<el-tab-pane label="课程列表" name="course" class="course-list-container">
|
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
|
<h3 class="text-lg font-semibold">课程列表</h3>
|
|
|
|
|
|
<p class="text-sm text-gray-500 mt-1">当前班级和学期下的可用课程</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="getCourseLists.lists.length === 0"
|
|
|
|
|
|
class="text-center py-8 text-gray-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
暂无课程数据,请选择班级和学期
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="space-y-4 max-h-[700px] overflow-y-auto">
|
|
|
|
|
|
<el-card
|
|
|
|
|
|
v-for="item in getCourseLists.lists"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
shadow="hover"
|
2025-12-29 06:15:08 +00:00
|
|
|
|
:class="['cursor-pointer transition-all duration-200', selectedCourseId === item.id ? 'selected-card' : 'hover:bg-blue-50']"
|
2025-12-26 08:15:02 +00:00
|
|
|
|
@click="handleCourseClick(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="p-0">
|
|
|
|
|
|
<div class="flex justify-between items-start">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h4 class="font-medium text-sm">
|
|
|
|
|
|
课程:{{ item.courseName }}
|
|
|
|
|
|
</h4>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex justify-between text-xs text-gray-600">
|
|
|
|
|
|
<span>教师: {{ item.teacherName }}</span>
|
|
|
|
|
|
<span>周学时: {{ item.totalWeeks }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="selectedCourseId === item.id"
|
|
|
|
|
|
class="pt-3 border-t border-gray-100"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="text-xs text-gray-700">
|
|
|
|
|
|
<div class="flex items-center mb-1">
|
|
|
|
|
|
<span class="font-medium">总学时:</span>
|
|
|
|
|
|
<span>{{ item.totalHours }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
|
<span class="font-medium">周数:</span>
|
|
|
|
|
|
<span>{{ item.totalWeeks }}周</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p
|
|
|
|
|
|
class="text-xs text-gray-500"
|
|
|
|
|
|
style="color: darkgray"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.taskCode }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex justify-end mt-6">
|
|
|
|
|
|
<pagination
|
|
|
|
|
|
layout="prev, pager, next"
|
|
|
|
|
|
v-model="pager"
|
|
|
|
|
|
@change="getLists"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane label="教室列表" name="classroom" class="course-list-container">
|
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
|
<h3 class="text-lg font-semibold">教室列表</h3>
|
|
|
|
|
|
<p class="text-sm text-gray-500 mt-1">当前可用的教室资源</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="getRoomLists.lists.length === 0"
|
|
|
|
|
|
class="text-center py-8 text-gray-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
暂无教室数据
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="space-y-4 max-h-[700px] overflow-y-auto">
|
|
|
|
|
|
<el-card
|
|
|
|
|
|
v-for="item in getRoomLists.lists"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
shadow="hover"
|
2025-12-29 06:15:08 +00:00
|
|
|
|
:class="['cursor-pointer transition-all duration-200', selectedRoomId === item.id ? 'selected-card' : 'hover:bg-blue-50']"
|
2025-12-26 08:15:02 +00:00
|
|
|
|
@click="handleClassroomClick(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="p-0">
|
|
|
|
|
|
<div class="flex justify-between items-start">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h4 class="font-medium text-sm">
|
|
|
|
|
|
教室:{{ item.classroomName }}
|
|
|
|
|
|
</h4>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex justify-between text-xs text-gray-600">
|
|
|
|
|
|
<span>类型: {{ item.classroomTypeName }}</span>
|
|
|
|
|
|
<span>容量: {{ item.capacity }}人</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="selectedRoomId === item.id"
|
|
|
|
|
|
class="pt-3 border-t border-gray-100"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="text-xs text-gray-700">
|
|
|
|
|
|
<div class="flex items-center mb-1">
|
|
|
|
|
|
<span class="font-medium">编号:</span>
|
|
|
|
|
|
<span>{{ item.classroomCode }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
|
<span class="font-medium">位置:</span>
|
|
|
|
|
|
<span>{{ item.location }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
2025-12-29 06:15:08 +00:00
|
|
|
|
<!-- 排课操作按钮 -->
|
|
|
|
|
|
<div v-if="selectedTimeSlot" class="mt-6 p-4 border-t">
|
|
|
|
|
|
<h4 class="text-lg font-semibold mb-4">排课操作</h4>
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
|
<div class="text-sm">
|
|
|
|
|
|
<div class="flex items-center mb-2">
|
|
|
|
|
|
<span class="font-medium mr-2">选中的时间段:</span>
|
|
|
|
|
|
<span>{{ getSelectedTimeSlotText() }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center mb-2">
|
|
|
|
|
|
<span class="font-medium mr-2">选中的课程:</span>
|
|
|
|
|
|
<span>{{ selectedCourseName || '未选择' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
|
<span class="font-medium mr-2">选中的教室:</span>
|
|
|
|
|
|
<span>{{ selectedRoomName || '未选择' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:disabled="!selectedCourseId"
|
|
|
|
|
|
@click="handleScheduleCourse"
|
|
|
|
|
|
plain
|
|
|
|
|
|
>
|
|
|
|
|
|
安排课程
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:disabled="!selectedTimeSlot.hasCourse"
|
|
|
|
|
|
@click="handleRemoveCourse"
|
|
|
|
|
|
plain
|
|
|
|
|
|
>
|
|
|
|
|
|
移除课程
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-26 08:15:02 +00:00
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
|
|
|
import { computed, reactive, ref } from 'vue'
|
2025-12-29 06:15:08 +00:00
|
|
|
|
import { Check } from '@element-plus/icons-vue'
|
2025-12-26 08:15:02 +00:00
|
|
|
|
|
|
|
|
|
|
import { classLists } from '@/api/class'
|
|
|
|
|
|
import { configLists } from '@/api/config'
|
2025-12-29 06:15:08 +00:00
|
|
|
|
import { courseAvailableRooms, courseAvailableSlots, courseSchedule } from '@/api/course'
|
2025-12-26 08:15:02 +00:00
|
|
|
|
import { taskLists } from '@/api/task'
|
|
|
|
|
|
import { timeCurrentSemester } from '@/api/time'
|
|
|
|
|
|
import { usePaging } from '@/hooks/usePaging'
|
|
|
|
|
|
import feedback from '@/utils/feedback'
|
|
|
|
|
|
|
2025-12-29 06:15:08 +00:00
|
|
|
|
// 星期数组
|
|
|
|
|
|
const weekDays = [
|
|
|
|
|
|
{ label: '星期一', value: 1 },
|
|
|
|
|
|
{ label: '星期二', value: 2 },
|
|
|
|
|
|
{ label: '星期三', value: 3 },
|
|
|
|
|
|
{ label: '星期四', value: 4 },
|
|
|
|
|
|
{ label: '星期五', value: 5 },
|
|
|
|
|
|
{ label: '星期六', value: 6 },
|
|
|
|
|
|
{ label: '星期日', value: 7 }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2025-12-26 08:15:02 +00:00
|
|
|
|
const getConfigLists = ref<{
|
|
|
|
|
|
lists: Array<{
|
|
|
|
|
|
id: number
|
|
|
|
|
|
academicYear: string
|
|
|
|
|
|
semesterCode: string
|
|
|
|
|
|
config: string
|
|
|
|
|
|
startDate: string
|
|
|
|
|
|
endDate: string
|
|
|
|
|
|
}>
|
|
|
|
|
|
}>({ lists: [] })
|
|
|
|
|
|
const getClassLists = ref<{
|
|
|
|
|
|
lists: Array<{ id: number; className: string; classCode: string; maxStudentCount: number }>
|
|
|
|
|
|
}>({ lists: [] })
|
|
|
|
|
|
const classSearchLoading = ref(false)
|
|
|
|
|
|
const classSearchTimer = ref<number | null>(null)
|
|
|
|
|
|
const activeTab = ref('course')
|
|
|
|
|
|
|
|
|
|
|
|
// 时间段数据
|
|
|
|
|
|
const getTimeData = ref<{
|
|
|
|
|
|
lists: Array<{
|
|
|
|
|
|
timeSlotId: number
|
|
|
|
|
|
classId: number
|
|
|
|
|
|
classroomId: number
|
|
|
|
|
|
courseId: number
|
|
|
|
|
|
semesterId: number
|
|
|
|
|
|
teacherId: number
|
|
|
|
|
|
}>
|
|
|
|
|
|
}>({ lists: [] })
|
|
|
|
|
|
|
|
|
|
|
|
// 课程列表数据
|
|
|
|
|
|
const getCourseLists = ref<{
|
|
|
|
|
|
lists: Array<{
|
|
|
|
|
|
id: number
|
|
|
|
|
|
courseId: number
|
|
|
|
|
|
courseName: string
|
|
|
|
|
|
taskCode: string
|
|
|
|
|
|
totalHours: number
|
|
|
|
|
|
teacherId: number
|
|
|
|
|
|
teacherName: string
|
|
|
|
|
|
totalWeeks: number
|
|
|
|
|
|
}>
|
|
|
|
|
|
}>({ lists: [] })
|
|
|
|
|
|
// 课程加载状态
|
|
|
|
|
|
const courseLoading = ref(false)
|
|
|
|
|
|
// 选中的课程ID
|
|
|
|
|
|
const selectedCourseId = ref<number | null>(null)
|
2025-12-29 06:15:08 +00:00
|
|
|
|
// 选中的课程名称
|
|
|
|
|
|
const selectedCourseName = computed(() => {
|
|
|
|
|
|
if (!selectedCourseId.value) return ''
|
|
|
|
|
|
const course = getCourseLists.value.lists.find(item => item.id === selectedCourseId.value)
|
|
|
|
|
|
return course ? course.courseName : ''
|
|
|
|
|
|
})
|
2025-12-26 08:15:02 +00:00
|
|
|
|
|
|
|
|
|
|
// 教室列表数据
|
|
|
|
|
|
const getRoomLists = ref<{
|
|
|
|
|
|
lists: Array<{
|
|
|
|
|
|
id: number
|
|
|
|
|
|
classroomName: string
|
|
|
|
|
|
classroomCode: string
|
|
|
|
|
|
classroomTypeName: string
|
|
|
|
|
|
location: string
|
|
|
|
|
|
capacity: number
|
|
|
|
|
|
}>
|
|
|
|
|
|
}>({ lists: [] })
|
|
|
|
|
|
// 教室加载状态
|
|
|
|
|
|
const roomLoading = ref(false)
|
|
|
|
|
|
// 选中的教室ID
|
|
|
|
|
|
const selectedRoomId = ref<number | null>(null)
|
2025-12-29 06:15:08 +00:00
|
|
|
|
// 选中的教室名称
|
|
|
|
|
|
const selectedRoomName = computed(() => {
|
|
|
|
|
|
if (!selectedRoomId.value) return ''
|
|
|
|
|
|
const room = getRoomLists.value.lists.find(item => item.id === selectedRoomId.value)
|
|
|
|
|
|
return room ? room.classroomName : ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 选中的时间段
|
|
|
|
|
|
interface SelectedTimeSlot {
|
|
|
|
|
|
id: number
|
|
|
|
|
|
rowIndex: number
|
|
|
|
|
|
dayOfWeek: number
|
|
|
|
|
|
startTime: string
|
|
|
|
|
|
endTime: string
|
|
|
|
|
|
hasCourse: boolean
|
|
|
|
|
|
courseData?: any
|
|
|
|
|
|
}
|
|
|
|
|
|
const selectedTimeSlot = ref<SelectedTimeSlot | null>(null)
|
2025-12-26 08:15:02 +00:00
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const queryParams = reactive({
|
|
|
|
|
|
classId: '',
|
|
|
|
|
|
semesterId: '',
|
|
|
|
|
|
maxStudentCount: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 班级变化处理
|
|
|
|
|
|
const handleClassChange = async () => {
|
|
|
|
|
|
if (queryParams.classId && queryParams.semesterId) {
|
|
|
|
|
|
await fetchCourseLists()
|
|
|
|
|
|
await fetchRoomLists()
|
|
|
|
|
|
}
|
2025-12-29 06:15:08 +00:00
|
|
|
|
// 切换班级时清空选中
|
|
|
|
|
|
clearSelectedTimeSlot()
|
2025-12-26 08:15:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleStudentClick = (item: any) => {
|
|
|
|
|
|
queryParams.maxStudentCount = item.maxStudentCount || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 学期变化处理
|
|
|
|
|
|
const handleSemesterChange = async () => {
|
|
|
|
|
|
if (queryParams.classId && queryParams.semesterId) {
|
|
|
|
|
|
await fetchCourseLists()
|
|
|
|
|
|
}
|
2025-12-29 06:15:08 +00:00
|
|
|
|
// 切换学期时清空选中
|
|
|
|
|
|
clearSelectedTimeSlot()
|
2025-12-26 08:15:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取班级列表
|
|
|
|
|
|
const fetchClassLists = async (params: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await classLists({
|
|
|
|
|
|
className: params.trim()
|
|
|
|
|
|
})
|
|
|
|
|
|
getClassLists.value = res
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
feedback.msgError('获取课程列表失败')
|
|
|
|
|
|
getClassLists.value = { lists: [] }
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
classSearchLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const handleClassRemoteSearch = (keyword: string) => {
|
|
|
|
|
|
if (classSearchTimer.value) clearTimeout(classSearchTimer.value)
|
|
|
|
|
|
|
|
|
|
|
|
classSearchTimer.value = setTimeout(async () => {
|
|
|
|
|
|
classSearchLoading.value = true
|
|
|
|
|
|
await fetchClassLists(keyword)
|
|
|
|
|
|
}, 300)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取学期列表
|
|
|
|
|
|
const fetchConfigLists = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await configLists()
|
|
|
|
|
|
const handleData = JSON.parse(JSON.stringify(res))
|
|
|
|
|
|
handleData.lists = handleData.lists.map((item: any) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
config: `${item.academicYear}${item.semesterCode == 'SPRING' ? ' 春' : ' 秋'}`
|
|
|
|
|
|
}))
|
|
|
|
|
|
getConfigLists.value = handleData
|
|
|
|
|
|
await fetchSemester()
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
feedback.msgError('获取学期列表失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fetchSemester = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await timeCurrentSemester()
|
|
|
|
|
|
if (res) {
|
|
|
|
|
|
queryParams.semesterId = res.id
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取学期信息失败', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取课程列表
|
|
|
|
|
|
const fetchCourseLists = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
courseLoading.value = true
|
|
|
|
|
|
// 使用taskLists接口获取课程列表
|
|
|
|
|
|
const response = await taskLists({
|
|
|
|
|
|
classId: queryParams.classId,
|
|
|
|
|
|
semesterId: queryParams.semesterId
|
|
|
|
|
|
})
|
|
|
|
|
|
// 映射新接口返回的数据格式到组件所需格式
|
|
|
|
|
|
const mappedCourses = response.lists.map(
|
|
|
|
|
|
(task: {
|
|
|
|
|
|
id: any
|
|
|
|
|
|
courseId: any
|
|
|
|
|
|
courseName: any
|
|
|
|
|
|
taskCode: any
|
|
|
|
|
|
weeklyHours: number
|
|
|
|
|
|
totalWeeks: number
|
|
|
|
|
|
teacherId: any
|
|
|
|
|
|
teacherName: any
|
|
|
|
|
|
}) => ({
|
|
|
|
|
|
id: task.id,
|
|
|
|
|
|
courseId: task.courseId,
|
|
|
|
|
|
courseName: task.courseName,
|
|
|
|
|
|
taskCode: task.taskCode,
|
|
|
|
|
|
totalHours: task.weeklyHours * task.totalWeeks, // 总学时=周学时×总周数
|
|
|
|
|
|
teacherId: task.teacherId,
|
|
|
|
|
|
teacherName: task.teacherName,
|
|
|
|
|
|
totalWeeks: task.totalWeeks
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
getCourseLists.value = { lists: mappedCourses }
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.log('获取课程列表失败', err)
|
|
|
|
|
|
feedback.msgError('获取课程列表失败')
|
|
|
|
|
|
getCourseLists.value = { lists: [] }
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
courseLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 课程点击处理
|
|
|
|
|
|
const handleCourseClick = (course: any) => {
|
|
|
|
|
|
selectedCourseId.value = selectedCourseId.value === course.id ? null : course.id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取教室列表
|
|
|
|
|
|
const fetchRoomLists = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
roomLoading.value = true
|
|
|
|
|
|
// 使用taskLists接口获取教室列表
|
|
|
|
|
|
const response = await courseAvailableRooms({
|
|
|
|
|
|
num: queryParams.maxStudentCount
|
|
|
|
|
|
})
|
|
|
|
|
|
// 映射新接口返回的数据格式到组件所需格式
|
|
|
|
|
|
const mappedRooms = response.map(
|
|
|
|
|
|
(task: {
|
|
|
|
|
|
id: number
|
|
|
|
|
|
classroomName: string
|
|
|
|
|
|
classroomCode: string
|
|
|
|
|
|
classroomTypeName: string
|
|
|
|
|
|
location: string
|
|
|
|
|
|
capacity: number
|
|
|
|
|
|
}) => ({
|
|
|
|
|
|
id: task.id,
|
|
|
|
|
|
classroomName: task.classroomName,
|
|
|
|
|
|
classroomCode: task.classroomCode,
|
|
|
|
|
|
classroomTypeName: task.classroomTypeName,
|
|
|
|
|
|
location: task.location,
|
|
|
|
|
|
capacity: task.capacity
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
getRoomLists.value = { lists: mappedRooms }
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.log('获取教室列表失败', err)
|
|
|
|
|
|
feedback.msgError('获取教室列表失败')
|
|
|
|
|
|
getRoomLists.value = { lists: [] }
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
roomLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 教室点击处理
|
|
|
|
|
|
const handleClassroomClick = (classroom: any) => {
|
|
|
|
|
|
selectedRoomId.value = selectedRoomId.value === classroom.id ? null : classroom.id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 06:15:08 +00:00
|
|
|
|
// 处理时间段点击
|
|
|
|
|
|
const handleTimeSlotClick = (rowIndex: number, dayOfWeek: number, cellData: any) => {
|
|
|
|
|
|
const row = tableData.value[rowIndex]
|
|
|
|
|
|
const id = cellData.id
|
|
|
|
|
|
const startTime = getRowStartTime(row)
|
|
|
|
|
|
const endTime = getRowEndTime(row)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果点击的是已经选中的时间段,则取消选中
|
|
|
|
|
|
if (selectedTimeSlot.value &&
|
|
|
|
|
|
selectedTimeSlot.value.rowIndex === rowIndex &&
|
|
|
|
|
|
selectedTimeSlot.value.dayOfWeek === dayOfWeek) {
|
|
|
|
|
|
selectedTimeSlot.value = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置选中的时间段
|
|
|
|
|
|
selectedTimeSlot.value = {
|
|
|
|
|
|
id,
|
|
|
|
|
|
rowIndex,
|
|
|
|
|
|
dayOfWeek,
|
|
|
|
|
|
startTime,
|
|
|
|
|
|
endTime,
|
|
|
|
|
|
hasCourse: !!cellData,
|
|
|
|
|
|
courseData: cellData
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有课程数据,自动选中对应的课程
|
|
|
|
|
|
if (cellData && cellData.courseId) {
|
|
|
|
|
|
const course = getCourseLists.value.lists.find(item => item.courseId === cellData.courseId)
|
|
|
|
|
|
if (course) {
|
|
|
|
|
|
selectedCourseId.value = course.id
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 判断时间段是否被选中
|
|
|
|
|
|
const isTimeSlotSelected = (rowIndex: number, dayOfWeek: number, cellData: any) => {
|
|
|
|
|
|
return selectedTimeSlot.value &&
|
|
|
|
|
|
selectedTimeSlot.value.rowIndex === rowIndex &&
|
|
|
|
|
|
selectedTimeSlot.value.dayOfWeek === dayOfWeek
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清除选中的时间段
|
|
|
|
|
|
const clearSelectedTimeSlot = () => {
|
|
|
|
|
|
selectedTimeSlot.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取选中时间段的显示文本
|
|
|
|
|
|
const getSelectedTimeSlotText = () => {
|
|
|
|
|
|
if (!selectedTimeSlot.value) return ''
|
|
|
|
|
|
|
|
|
|
|
|
const dayText = weekDays.find(day => day.value === selectedTimeSlot.value!.dayOfWeek)?.label || ''
|
|
|
|
|
|
return `第${selectedTimeSlot.value.rowIndex + 1}节 ${dayText} ${selectedTimeSlot.value.startTime}-${selectedTimeSlot.value.endTime}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安排课程
|
|
|
|
|
|
const handleScheduleCourse = async () => {
|
|
|
|
|
|
if (!selectedTimeSlot.value || !selectedCourseId.value) {
|
|
|
|
|
|
feedback.msgError('请先选择时间段和课程')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 调用排课API
|
|
|
|
|
|
await courseSchedule({
|
|
|
|
|
|
timeSlotId: selectedTimeSlot.value.id,
|
|
|
|
|
|
courseId: selectedCourseId.value,
|
|
|
|
|
|
// date:
|
|
|
|
|
|
classroomId: selectedRoomId.value,
|
|
|
|
|
|
semesterId: queryParams.semesterId,
|
|
|
|
|
|
classId: queryParams.classId,
|
|
|
|
|
|
// teacherId:
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
feedback.msgSuccess('排课成功')
|
|
|
|
|
|
// 刷新课表数据
|
|
|
|
|
|
await getTimeLists()
|
|
|
|
|
|
// 清空选中状态
|
|
|
|
|
|
clearSelectedTimeSlot()
|
|
|
|
|
|
selectedCourseId.value = null
|
|
|
|
|
|
selectedRoomId.value = null
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
feedback.msgError('排课失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 移除课程
|
|
|
|
|
|
const handleRemoveCourse = async () => {
|
|
|
|
|
|
if (!selectedTimeSlot.value || !selectedTimeSlot.value.hasCourse) {
|
|
|
|
|
|
feedback.msgError('请先选择一个有课程的时间段')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// TODO: 这里调用移除课程API
|
|
|
|
|
|
// 示例:await removeCourse({
|
|
|
|
|
|
// timeSlotId: selectedTimeSlot.value.timeSlotId
|
|
|
|
|
|
// })
|
|
|
|
|
|
|
|
|
|
|
|
feedback.msgSuccess('课程移除成功')
|
|
|
|
|
|
// 刷新课表数据
|
|
|
|
|
|
await getTimeLists()
|
|
|
|
|
|
// 清空选中状态
|
|
|
|
|
|
clearSelectedTimeSlot()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
feedback.msgError('移除课程失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 08:15:02 +00:00
|
|
|
|
// 清理定时器
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
if (classSearchTimer.value) clearTimeout(classSearchTimer.value)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 所有数据列表
|
|
|
|
|
|
const allLists = ref<any[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
const { pager, getLists } = usePaging({
|
|
|
|
|
|
fetchFun: taskLists,
|
|
|
|
|
|
params: queryParams,
|
|
|
|
|
|
size: 7
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 请求数据列表
|
|
|
|
|
|
const getTimeLists = async () => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await courseAvailableSlots()
|
|
|
|
|
|
allLists.value = res
|
|
|
|
|
|
return Promise.resolve(res)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
return Promise.reject(err)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理表格数据,按节次分组,构建星期矩阵
|
|
|
|
|
|
const tableData = computed(() => {
|
|
|
|
|
|
const timeSlotMap = new Map<string, Record<number, any>>()
|
|
|
|
|
|
allLists.value.forEach((item) => {
|
|
|
|
|
|
const key = `${item.startTime}-${item.endTime}`
|
|
|
|
|
|
if (!timeSlotMap.has(key)) {
|
|
|
|
|
|
timeSlotMap.set(key, {})
|
|
|
|
|
|
}
|
|
|
|
|
|
const slotGroup = timeSlotMap.get(key)!
|
|
|
|
|
|
slotGroup[item.dayOfWeek] = item
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return Array.from(timeSlotMap.values()).sort((a, b) => {
|
|
|
|
|
|
const aTime = Object.values(a)[0]?.startTime || ''
|
|
|
|
|
|
const bTime = Object.values(b)[0]?.startTime || ''
|
|
|
|
|
|
return aTime.localeCompare(bTime)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const getRowBaseInfo = (row: Record<number, any>) => {
|
|
|
|
|
|
return Object.values(row).find((item) => item) || {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getRowStartTime = (row: Record<number, any>) => {
|
|
|
|
|
|
const baseInfo = getRowBaseInfo(row)
|
|
|
|
|
|
const start = baseInfo?.startTime
|
|
|
|
|
|
if (start == null) return ''
|
|
|
|
|
|
const s = String(start)
|
|
|
|
|
|
return s.length > 3 ? s.slice(0, -3) : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getRowEndTime = (row: Record<number, any>) => {
|
|
|
|
|
|
const baseInfo = getRowBaseInfo(row)
|
|
|
|
|
|
const end = baseInfo?.endTime
|
|
|
|
|
|
if (end == null) return ''
|
|
|
|
|
|
const s = String(end)
|
|
|
|
|
|
return s.length > 3 ? s.slice(0, -3) : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化时获取数据
|
|
|
|
|
|
getTimeLists()
|
|
|
|
|
|
fetchClassLists('')
|
|
|
|
|
|
fetchConfigLists()
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
:deep(.first-column) {
|
|
|
|
|
|
background-color: #f8f8f8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.course-list-container {
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.course-list-container .el-card {
|
|
|
|
|
|
border: 1px solid #e4e7ed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.course-list-container .el-card:hover {
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 06:15:08 +00:00
|
|
|
|
.selected-card {
|
|
|
|
|
|
border-color: #409eff !important;
|
|
|
|
|
|
background-color: #f0f7ff;
|
|
|
|
|
|
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 08:15:02 +00:00
|
|
|
|
:deep(.el-tag) {
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
line-height: 22px;
|
|
|
|
|
|
}
|
2025-12-29 06:15:08 +00:00
|
|
|
|
|
|
|
|
|
|
/* 时间段单元格样式 */
|
|
|
|
|
|
.time-slot-cell {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
border: 2px solid transparent;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.time-slot-cell:hover {
|
|
|
|
|
|
border-color: #96caff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.time-slot-selected {
|
|
|
|
|
|
border-color: #409eff !important;
|
|
|
|
|
|
background-color: #f0f9ff;
|
|
|
|
|
|
border-color: #e0f2ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-slot {
|
|
|
|
|
|
background-color: #fafafa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-slot-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-slot-content .check-icon {
|
|
|
|
|
|
color: #67c23a;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.course-info {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.course-name {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.course-details {
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.course-details div {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
2025-12-26 08:15:02 +00:00
|
|
|
|
</style>
|