kindergarten_java/reading-platform-frontend/src/views/school/schedule/TimetableView.vue
Claude Opus 4.6 c90873bea9 Merge remote-tracking branch 'origin/master' and complete two-tier structure refactoring
合并同事的远程更新:
- 多地点登录支持功能
- 资源库管理优化
- 数据看板修复
- 视频预览功能
- KidsMode增强

两层结构重构完成:
- 数据库迁移 V28(course_collection、course_collection_package)
- 后端实体、Service、Controller实现
- 前端API类型和组件重构
- 修复冲突文件:CHANGELOG.md、components.d.ts、TeacherLessonController.java

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:59:06 +08:00

404 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="timetable-view">
<div class="page-header">
<a-space>
<a-button @click="goToPrevWeek">
<template #icon><LeftOutlined /></template>
上一周
</a-button>
<a-button @click="goToCurrentWeek">本周</a-button>
<a-button @click="goToNextWeek">
<template #icon><RightOutlined /></template>
下一周
</a-button>
<a-divider type="vertical" />
<span class="week-range">周次{{ weekRangeText }}</span>
</a-space>
</div>
<!-- 筛选区 -->
<div class="filter-section">
<a-space>
<a-select
v-model:value="filters.classId"
placeholder="选择班级"
allowClear
style="width: 150px"
@change="loadTimetable"
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.teacherId"
placeholder="选择教师"
allowClear
style="width: 150px"
@change="loadTimetable"
>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
</a-select>
</a-space>
</div>
<!-- 课表 -->
<div class="timetable-container">
<div class="timetable-header">
<div
v-for="day in weekDays"
:key="day.date"
class="day-header"
:class="{ 'is-today': day.isToday }"
>
<div class="day-name">{{ day.dayName }}</div>
<div class="day-date">{{ day.dateDisplay }}</div>
</div>
</div>
<div class="timetable-body">
<a-spin :spinning="loading">
<div class="timetable-grid">
<div
v-for="day in weekDays"
:key="day.date"
class="day-column"
:class="{ 'is-today': day.isToday }"
>
<div
v-for="schedule in day.schedules"
:key="schedule.id"
class="schedule-card"
:class="{
'school-schedule': schedule.source === 'SCHOOL',
'teacher-schedule': schedule.source === 'TEACHER',
'cancelled': schedule.status === 'CANCELLED',
}"
@click="showScheduleDetail(schedule)"
>
<div class="schedule-time">{{ schedule.scheduledTime || '待定' }}</div>
<div class="schedule-course">{{ schedule.courseName }}</div>
<div class="schedule-class">{{ schedule.className }}</div>
<div v-if="schedule.teacherName" class="schedule-teacher">
{{ schedule.teacherName }}
</div>
<a-tag v-if="schedule.status === 'CANCELLED'" color="error" size="small">已取消</a-tag>
</div>
<div v-if="!day.schedules.length" class="empty-day">
暂无排课
</div>
</div>
</div>
</a-spin>
</div>
</div>
<!-- 排课详情弹窗 -->
<a-modal
v-model:open="detailVisible"
title="排课详情"
:footer="null"
width="500px"
>
<template v-if="selectedSchedule">
<a-descriptions :column="1" bordered>
<a-descriptions-item label="班级">{{ selectedSchedule.className }}</a-descriptions-item>
<a-descriptions-item label="课程">{{ selectedSchedule.courseName }}</a-descriptions-item>
<a-descriptions-item label="授课教师">{{ selectedSchedule.teacherName || '未分配' }}</a-descriptions-item>
<a-descriptions-item label="排课日期">{{ formatDate(selectedSchedule.scheduledDate) }}</a-descriptions-item>
<a-descriptions-item label="时间段">{{ selectedSchedule.scheduledTime || '待定' }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag v-if="selectedSchedule.status === 'ACTIVE'" color="success">有效</a-tag>
<a-tag v-else color="error">已取消</a-tag>
</a-descriptions-item>
</a-descriptions>
</template>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
import {
LeftOutlined,
RightOutlined,
} from '@ant-design/icons-vue';
import {
getTimetable,
getClasses,
getTeachers,
type TimetableItem,
type SchedulePlan,
type ClassInfo,
type Teacher,
} from '@/api/school';
// 数据
const loading = ref(false);
const timetableData = ref<{
byDate: Record<string, SchedulePlan[]>;
byWeekDay: Record<number, SchedulePlan[]>;
total: number;
}>({ byDate: {}, byWeekDay: {}, total: 0 });
const classes = ref<ClassInfo[]>([]);
const teachers = ref<Teacher[]>([]);
// 当前周
const currentWeekStart = ref(dayjs().startOf('week').add(1, 'day')); // 周一
// 筛选
const filters = reactive({
classId: undefined as number | undefined,
teacherId: undefined as number | undefined,
});
// 详情弹窗
const detailVisible = ref(false);
const selectedSchedule = ref<SchedulePlan | null>(null);
// 计算属性
const weekRangeText = computed(() => {
const start = currentWeekStart.value;
const end = start.add(6, 'day');
return `${start.format('MM-DD')} 至 ${end.format('MM-DD')}`;
});
const weekDays = computed(() => {
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const today = dayjs().format('YYYY-MM-DD');
return Array.from({ length: 7 }, (_, i) => {
const date = currentWeekStart.value.add(i, 'day');
const dateStr = date.format('YYYY-MM-DD');
const schedules = timetableData.value.byDate[dateStr] || [];
return {
date: dateStr,
dayName: days[i],
dateDisplay: date.format('MM-DD'),
isToday: dateStr === today,
schedules: schedules,
};
});
});
// 加载数据
const loadTimetable = async () => {
loading.value = true;
try {
const start = currentWeekStart.value;
const res = await getTimetable({
startDate: start.format('YYYY-MM-DD'),
endDate: start.add(6, 'day').format('YYYY-MM-DD'),
...filters,
});
timetableData.value = res;
} catch (error) {
message.error('加载课表失败');
} finally {
loading.value = false;
}
};
const loadBaseData = async () => {
try {
const [classesRes, teachersRes] = await Promise.all([
getClasses(),
getTeachers({ pageNum: 1, pageSize: 100 }),
]);
classes.value = classesRes;
teachers.value = teachersRes.list;
} catch (error) {
message.error('加载基础数据失败');
}
};
// 周次导航
const goToPrevWeek = () => {
currentWeekStart.value = currentWeekStart.value.subtract(7, 'day');
loadTimetable();
};
const goToNextWeek = () => {
currentWeekStart.value = currentWeekStart.value.add(7, 'day');
loadTimetable();
};
const goToCurrentWeek = () => {
currentWeekStart.value = dayjs().startOf('week').add(1, 'day');
loadTimetable();
};
// 详情
const showScheduleDetail = (schedule: SchedulePlan) => {
selectedSchedule.value = schedule;
detailVisible.value = true;
};
const formatDate = (date: string | undefined) => {
if (!date) return '-';
return dayjs(date).format('YYYY-MM-DD');
};
// 暴露刷新方法给父组件
defineExpose({
refresh: loadTimetable,
});
onMounted(() => {
loadBaseData();
loadTimetable();
});
</script>
<style scoped lang="scss">
.timetable-view {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.week-range {
font-weight: 500;
color: #333;
}
}
.filter-section {
margin-bottom: 20px;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.timetable-container {
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
}
.timetable-header {
display: table;
width: 100%;
table-layout: fixed;
background: linear-gradient(135deg, #FF8C42 0%, #E67635 100%);
color: white;
.day-header {
display: table-cell;
padding: 12px 8px;
text-align: center;
border-right: 1px solid rgba(255, 255, 255, 0.2);
vertical-align: middle;
&:last-child {
border-right: none;
}
&.is-today {
background: rgba(255, 255, 255, 0.2);
}
.day-name {
font-weight: 500;
}
.day-date {
font-size: 12px;
opacity: 0.8;
}
}
}
.timetable-body {
min-height: 400px;
}
.timetable-grid {
display: table;
width: 100%;
table-layout: fixed;
.day-column {
display: table-cell;
min-height: 300px;
padding: 8px;
border-right: 1px solid #e8e8e8;
background: #fafafa;
vertical-align: top;
&:last-child {
border-right: none;
}
&.is-today {
background: #fff4ec;
}
}
}
.schedule-card {
background: white;
border-radius: 8px;
padding: 10px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s;
border-left: 3px solid #FF8C42;
&:hover {
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
&.teacher-schedule {
border-left-color: #722ed1;
}
&.cancelled {
opacity: 0.5;
border-left-color: #999;
}
.schedule-time {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.schedule-course {
font-weight: 500;
color: #333;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.schedule-class {
font-size: 12px;
color: #666;
}
.schedule-teacher {
font-size: 12px;
color: #999;
margin-top: 4px;
}
}
.empty-day {
text-align: center;
color: #999;
padding: 20px;
font-size: 12px;
}
}
</style>