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.

754 lines
22 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="course-table-container">
<div class="course-header">
<h1 class="page-title">课程表</h1>
<div class="course-actions">
<el-button type="primary" @click="handleAddCourse" v-if="isLoggedIn">
<el-icon><Plus /></el-icon>添加课程
</el-button>
</div>
</div>
<div class="course-filters">
<el-card shadow="never" class="filter-card">
<div class="filter-row">
<div class="semester-selector">
<span class="filter-label">学期:</span>
<el-select v-model="currentSemester" placeholder="选择学期">
<el-option label="2023-2024学年第一学期" value="2023-1"></el-option>
<el-option label="2023-2024学年第二学期" value="2023-2"></el-option>
</el-select>
</div>
<div class="week-selector">
<span class="filter-label">周次:</span>
<el-select v-model="currentWeek" placeholder="选择周次">
<el-option v-for="week in 20" :key="week" :label="`第${week}周`" :value="week"></el-option>
</el-select>
</div>
<div class="view-selector">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="week">周视图</el-radio-button>
<el-radio-button label="day">日视图</el-radio-button>
</el-radio-group>
</div>
</div>
</el-card>
</div>
<!-- 周视图 -->
<div v-if="viewMode === 'week'" class="week-view">
<el-card shadow="hover" class="timetable-card">
<div class="timetable">
<!-- 时间列 -->
<div class="time-column">
<div class="header-cell"></div>
<div class="time-cell" v-for="time in timeSlots" :key="time.id">
{{ time.label }}
</div>
</div>
<!-- 星期列 -->
<div v-for="day in 7" :key="day" class="day-column">
<div class="header-cell">{{ getDayLabel(day) }}</div>
<div
class="course-cell"
v-for="time in timeSlots"
:key="time.id"
@click="handleCellClick(day, time.id)"
>
<div
v-for="course in getCoursesForTimeSlot(day, time.id)"
:key="course.id"
class="course-item"
:style="{ backgroundColor: course.color || '#409EFF' }"
@click.stop="handleCourseClick(course)"
>
<div class="course-name">{{ course.name }}</div>
<div class="course-info">{{ course.location }}</div>
<div class="course-info">{{ course.teacher }}</div>
</div>
</div>
</div>
</div>
</el-card>
</div>
<!-- 日视图 -->
<div v-else class="day-view">
<el-card shadow="hover" class="day-card">
<div class="day-header">
<el-button-group>
<el-button @click="previousDay">
<el-icon><ArrowLeft /></el-icon>
</el-button>
<el-button>{{ getDayLabel(currentDay) }}</el-button>
<el-button @click="nextDay">
<el-icon><ArrowRight /></el-icon>
</el-button>
</el-button-group>
</div>
<div class="day-timetable">
<div class="time-column">
<div class="time-cell" v-for="time in timeSlots" :key="time.id">
{{ time.label }}
</div>
</div>
<div class="day-courses-column">
<div
class="course-cell"
v-for="time in timeSlots"
:key="time.id"
@click="handleCellClick(currentDay, time.id)"
>
<div
v-for="course in getCoursesForTimeSlot(currentDay, time.id)"
:key="course.id"
class="course-item day-course-item"
:style="{ backgroundColor: course.color || '#409EFF' }"
@click.stop="handleCourseClick(course)"
>
<div class="course-name">{{ course.name }}</div>
<div class="course-info">{{ course.location }}</div>
<div class="course-info">{{ course.teacher }}</div>
</div>
</div>
</div>
</div>
</el-card>
</div>
<!-- 添加/编辑课程对话框 -->
<el-dialog
v-model="courseDialogVisible"
:title="isEditing ? '编辑课程' : '添加课程'"
width="500px"
>
<el-form :model="courseForm" label-width="80px" :rules="courseRules" ref="courseFormRef">
<el-form-item label="课程名称" prop="name">
<el-input v-model="courseForm.name" placeholder="请输入课程名称"></el-input>
</el-form-item>
<el-form-item label="教师" prop="teacher">
<el-input v-model="courseForm.teacher" placeholder="请输入教师姓名"></el-input>
</el-form-item>
<el-form-item label="地点" prop="location">
<el-input v-model="courseForm.location" placeholder="请输入上课地点"></el-input>
</el-form-item>
<el-form-item label="星期" prop="dayOfWeek">
<el-select v-model="courseForm.dayOfWeek" placeholder="请选择星期">
<el-option v-for="day in 7" :key="day" :label="getDayLabel(day)" :value="day"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-time-select
v-model="courseForm.startTime"
placeholder="请选择开始时间"
start="08:00"
step="00:30"
end="22:00"
></el-time-select>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-time-select
v-model="courseForm.endTime"
placeholder="请选择结束时间"
start="08:00"
step="00:30"
end="22:00"
:min-time="courseForm.startTime"
></el-time-select>
</el-form-item>
<el-form-item label="开始周次" prop="startWeek">
<el-input-number v-model="courseForm.startWeek" :min="1" :max="20"></el-input-number>
</el-form-item>
<el-form-item label="结束周次" prop="endWeek">
<el-input-number v-model="courseForm.endWeek" :min="courseForm.startWeek" :max="20"></el-input-number>
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="courseForm.color"></el-color-picker>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="courseDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitCourse" :loading="submitting">
</el-button>
</span>
</template>
</el-dialog>
<!-- 课程详情对话框 -->
<el-dialog
v-model="courseDetailVisible"
title="课程详情"
width="400px"
>
<div v-if="selectedCourse" class="course-detail">
<h3 class="detail-title">{{ selectedCourse.name }}</h3>
<div class="detail-item">
<span class="detail-label">教师:</span>
<span>{{ selectedCourse.teacher || '未设置' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">地点:</span>
<span>{{ selectedCourse.location || '未设置' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">时间:</span>
<span>{{ getDayLabel(selectedCourse.dayOfWeek) }} {{ selectedCourse.startTime }} - {{ selectedCourse.endTime }}</span>
</div>
<div class="detail-item">
<span class="detail-label">周次:</span>
<span>第{{ selectedCourse.startWeek }}周 - 第{{ selectedCourse.endWeek }}周</span>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="courseDetailVisible = false">关闭</el-button>
<el-button type="primary" @click="handleEditCourse" v-if="isOwner">编辑</el-button>
<el-button type="danger" @click="handleDeleteCourse" v-if="isOwner">删除</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
import { scheduleApi } from '@/api';
import { useUserStore } from '@/stores';
import { Plus, ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
const userStore = useUserStore();
const isLoggedIn = computed(() => userStore.isLoggedIn);
// 课程表数据
const courses = ref<any[]>([]);
const loading = ref(false);
const currentSemester = ref('2023-1');
const currentWeek = ref(1);
const viewMode = ref('week');
const currentDay = ref(1); // 1-7 表示周一到周日
// 时间段定义
const timeSlots = [
{ id: 1, label: '第1节 8:00-8:45' },
{ id: 2, label: '第2节 8:55-9:40' },
{ id: 3, label: '第3节 10:00-10:45' },
{ id: 4, label: '第4节 10:55-11:40' },
{ id: 5, label: '第5节 13:30-14:15' },
{ id: 6, label: '第6节 14:25-15:10' },
{ id: 7, label: '第7节 15:30-16:15' },
{ id: 8, label: '第8节 16:25-17:10' },
{ id: 9, label: '第9节 18:30-19:15' },
{ id: 10, label: '第10节 19:25-20:10' },
{ id: 11, label: '第11节 20:20-21:05' },
{ id: 12, label: '第12节 21:15-22:00' }
];
// 课程表单
const courseDialogVisible = ref(false);
const courseFormRef = ref<FormInstance>();
const submitting = ref(false);
const isEditing = ref(false);
const courseForm = reactive({
id: undefined as number | undefined,
name: '',
teacher: '',
location: '',
dayOfWeek: 1,
startTime: '',
endTime: '',
startWeek: 1,
endWeek: 16,
color: '#409EFF'
});
const courseRules = {
name: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
dayOfWeek: [{ required: true, message: '请选择星期', trigger: 'change' }],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
startWeek: [{ required: true, message: '请输入开始周次', trigger: 'blur' }],
endWeek: [{ required: true, message: '请输入结束周次', trigger: 'blur' }]
};
// 课程详情
const courseDetailVisible = ref(false);
const selectedCourse = ref<any>(null);
const isOwner = computed(() => {
if (!selectedCourse.value || !userStore.user) return false;
return selectedCourse.value.userId === userStore.user.id;
});
// 初始化
onMounted(async () => {
await fetchCourses();
});
// 获取课程列表
const fetchCourses = async () => {
loading.value = true;
try {
const res = await scheduleApi.getAllCourses();
if (res.code === 200) {
courses.value = res.data.list;
}
} catch (error) {
console.error('获取课程列表失败:', error);
ElMessage.error('获取课程列表失败');
} finally {
loading.value = false;
}
};
// 获取星期标签
const getDayLabel = (day: number) => {
const days = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'];
return days[day] || '';
};
// 获取指定时间段的课程
const getCoursesForTimeSlot = (day: number, timeSlotId: number) => {
return courses.value.filter(course => {
// 检查星期是否匹配
if (course.dayOfWeek !== day) return false;
// 检查周次是否匹配
if (currentWeek.value < course.startWeek || currentWeek.value > course.endWeek) return false;
// 检查时间段是否匹配
const courseStartHour = parseInt(course.startTime.split(':')[0]);
const courseStartMinute = parseInt(course.startTime.split(':')[1]);
const courseEndHour = parseInt(course.endTime.split(':')[0]);
const courseEndMinute = parseInt(course.endTime.split(':')[1]);
const slotStartHour = parseInt(timeSlots[timeSlotId - 1].label.split(' ')[1].split('-')[0].split(':')[0]);
const slotStartMinute = parseInt(timeSlots[timeSlotId - 1].label.split(' ')[1].split('-')[0].split(':')[1]);
const slotEndHour = parseInt(timeSlots[timeSlotId - 1].label.split(' ')[1].split('-')[1].split(':')[0]);
const slotEndMinute = parseInt(timeSlots[timeSlotId - 1].label.split(' ')[1].split('-')[1].split(':')[1]);
const courseStartTime = courseStartHour * 60 + courseStartMinute;
const courseEndTime = courseEndHour * 60 + courseEndMinute;
const slotStartTime = slotStartHour * 60 + slotStartMinute;
const slotEndTime = slotEndHour * 60 + slotEndMinute;
// 如果课程时间与时间段有重叠则返回true
return (courseStartTime <= slotEndTime && courseEndTime >= slotStartTime);
});
};
// 处理单元格点击
const handleCellClick = (day: number, timeSlotId: number) => {
if (!isLoggedIn.value) {
ElMessage.warning('请先登录');
return;
}
// 设置表单初始值
isEditing.value = false;
courseForm.id = undefined;
courseForm.name = '';
courseForm.teacher = '';
courseForm.location = '';
courseForm.dayOfWeek = day;
// 设置时间
const timeSlot = timeSlots[timeSlotId - 1];
const timeRange = timeSlot.label.split(' ')[1].split('-');
courseForm.startTime = timeRange[0];
courseForm.endTime = timeRange[1];
courseForm.startWeek = currentWeek.value;
courseForm.endWeek = currentWeek.value + 15 > 20 ? 20 : currentWeek.value + 15;
courseForm.color = getRandomColor();
courseDialogVisible.value = true;
};
// 处理课程点击
const handleCourseClick = (course: any) => {
selectedCourse.value = course;
courseDetailVisible.value = true;
};
// 处理添加课程
const handleAddCourse = () => {
if (!isLoggedIn.value) {
ElMessage.warning('请先登录');
return;
}
isEditing.value = false;
courseForm.id = undefined;
courseForm.name = '';
courseForm.teacher = '';
courseForm.location = '';
courseForm.dayOfWeek = 1;
courseForm.startTime = '08:00';
courseForm.endTime = '09:40';
courseForm.startWeek = 1;
courseForm.endWeek = 16;
courseForm.color = getRandomColor();
courseDialogVisible.value = true;
};
// 处理编辑课程
const handleEditCourse = () => {
if (!selectedCourse.value) return;
isEditing.value = true;
courseForm.id = selectedCourse.value.id;
courseForm.name = selectedCourse.value.name;
courseForm.teacher = selectedCourse.value.teacher || '';
courseForm.location = selectedCourse.value.location || '';
courseForm.dayOfWeek = selectedCourse.value.dayOfWeek;
courseForm.startTime = selectedCourse.value.startTime;
courseForm.endTime = selectedCourse.value.endTime;
courseForm.startWeek = selectedCourse.value.startWeek;
courseForm.endWeek = selectedCourse.value.endWeek;
courseForm.color = selectedCourse.value.color || '#409EFF';
courseDetailVisible.value = false;
courseDialogVisible.value = true;
};
// 处理删除课程
const handleDeleteCourse = () => {
if (!selectedCourse.value) return;
ElMessageBox.confirm(
'确定要删除该课程吗?此操作不可恢复',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const res = await scheduleApi.deleteCourse(selectedCourse.value.id);
if (res.code === 200) {
ElMessage.success('课程删除成功');
courseDetailVisible.value = false;
fetchCourses();
}
} catch (error) {
console.error('删除课程失败:', error);
ElMessage.error('删除课程失败');
}
}).catch(() => {
// 用户取消删除
});
};
// 提交课程表单
const submitCourse = async () => {
if (!courseFormRef.value) return;
await courseFormRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true;
try {
// 检查时间冲突
const conflictParams = {
dayOfWeek: courseForm.dayOfWeek,
startTime: courseForm.startTime,
endTime: courseForm.endTime,
excludeCourseId: courseForm.id
};
const conflictRes = await scheduleApi.checkCourseConflict(conflictParams);
if (conflictRes.code === 200 && conflictRes.data.hasConflict) {
ElMessageBox.confirm(
`检测到时间冲突,有${conflictRes.data.conflictCount}门课程与当前时间段冲突,是否继续?`,
'时间冲突',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
saveCourse();
}).catch(() => {
submitting.value = false;
});
} else {
saveCourse();
}
} catch (error) {
console.error('检查时间冲突失败:', error);
ElMessage.error('检查时间冲突失败');
submitting.value = false;
}
}
});
};
// 保存课程
const saveCourse = async () => {
try {
let res;
if (isEditing.value && courseForm.id) {
// 更新课程
res = await scheduleApi.updateCourse(courseForm.id, {
name: courseForm.name,
teacher: courseForm.teacher,
location: courseForm.location,
dayOfWeek: courseForm.dayOfWeek,
startTime: courseForm.startTime,
endTime: courseForm.endTime,
startWeek: courseForm.startWeek,
endWeek: courseForm.endWeek,
color: courseForm.color
});
} else {
// 创建课程
res = await scheduleApi.createCourse({
name: courseForm.name,
teacher: courseForm.teacher,
location: courseForm.location,
dayOfWeek: courseForm.dayOfWeek,
startTime: courseForm.startTime,
endTime: courseForm.endTime,
startWeek: courseForm.startWeek,
endWeek: courseForm.endWeek,
color: courseForm.color
});
}
if (res.code === 200) {
ElMessage.success(isEditing.value ? '课程更新成功' : '课程添加成功');
courseDialogVisible.value = false;
fetchCourses();
}
} catch (error) {
console.error(isEditing.value ? '更新课程失败:' : '添加课程失败:', error);
ElMessage.error(isEditing.value ? '更新课程失败' : '添加课程失败');
} finally {
submitting.value = false;
}
};
// 切换到前一天
const previousDay = () => {
currentDay.value = currentDay.value === 1 ? 7 : currentDay.value - 1;
};
// 切换到后一天
const nextDay = () => {
currentDay.value = currentDay.value === 7 ? 1 : currentDay.value + 1;
};
// 获取随机颜色
const getRandomColor = () => {
const colors = [
'#409EFF', // 蓝色
'#67C23A', // 绿色
'#E6A23C', // 黄色
'#F56C6C', // 红色
'#909399', // 灰色
'#9966CC', // 紫色
'#FF9900', // 橙色
'#19CAAD', // 青色
'#8CC7B5', // 浅绿
'#A0EEE1', // 浅青
'#BEE7E9', // 浅蓝
'#BEEDC7', // 浅绿
'#D6D5B7', // 浅黄
'#D1BA74', // 浅棕
'#E6CEAC', // 浅橙
'#ECAD9E' // 浅红
];
return colors[Math.floor(Math.random() * colors.length)];
};
</script>
<style scoped>
.course-table-container {
padding: 20px;
}
.course-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
margin: 0;
}
.course-filters {
margin-bottom: 20px;
}
.filter-card {
border-radius: 8px;
}
.filter-row {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
}
.filter-label {
margin-right: 10px;
font-weight: 500;
}
/* 周视图样式 */
.week-view {
margin-top: 20px;
}
.timetable-card {
border-radius: 8px;
}
.timetable {
display: flex;
min-height: 800px;
}
.time-column, .day-column {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #eee;
}
.time-column {
flex: 0 0 120px;
}
.header-cell {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
background-color: #f5f7fa;
border-bottom: 1px solid #eee;
}
.time-cell {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
font-size: 12px;
}
.course-cell {
height: 60px;
border-bottom: 1px solid #eee;
position: relative;
cursor: pointer;
}
.course-item {
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
border-radius: 4px;
padding: 4px;
color: white;
font-size: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
}
.course-item:hover {
transform: scale(1.02);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.course-name {
font-weight: bold;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.course-info {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 10px;
}
/* 日视图样式 */
.day-view {
margin-top: 20px;
}
.day-card {
border-radius: 8px;
}
.day-header {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.day-timetable {
display: flex;
min-height: 800px;
}
.day-courses-column {
flex: 1;
display: flex;
flex-direction: column;
}
.day-course-item {
padding: 8px;
}
/* 课程详情样式 */
.course-detail {
padding: 10px;
}
.detail-title {
font-size: 18px;
margin-bottom: 15px;
color: #303133;
}
.detail-item {
margin-bottom: 10px;
display: flex;
}
.detail-label {
font-weight: 500;
width: 60px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>