kindergarten_java/reading-platform-frontend/src/views/school/schedule/CalendarView.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

594 lines
15 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="calendar-view">
<div class="page-header">
<a-space>
<a-radio-group v-model:value="viewType" button-style="solid" size="small">
<a-radio-button value="month"></a-radio-button>
<a-radio-button value="week"></a-radio-button>
</a-radio-group>
<a-divider type="vertical" />
<a-button @click="prevNav">
<template #icon><LeftOutlined /></template>
</a-button>
<span class="current-range-label">{{ currentRangeLabel }}</span>
<a-button @click="nextNav">
<template #icon><RightOutlined /></template>
</a-button>
</a-space>
<a-space>
<a-select
v-model:value="selectedClassId"
placeholder="筛选班级"
allowClear
style="width: 150px"
@change="loadEvents"
>
<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="selectedTeacherId"
placeholder="筛选教师"
allowClear
style="width: 150px"
@change="loadEvents"
>
<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 v-if="viewType === 'month'" class="calendar-month">
<div class="weekday-header">
<div v-for="day in weekdayNames" :key="day" class="weekday-item">{{ day }}</div>
</div>
<div class="calendar-grid">
<div
v-for="date in calendarDays"
:key="date.key"
:class="['calendar-day', { today: date.isToday, 'other-month': date.isOtherMonth }]"
@click="handleDayClick(date)"
>
<div class="day-number">{{ date.dayNumber }}</div>
<div v-if="date.hasSchedule" class="schedule-indicator"></div>
<div v-if="date.scheduleCount > 0" class="schedule-count">{{ date.scheduleCount }}</div>
</div>
</div>
</div>
<!-- 周视图 -->
<div v-else class="calendar-week">
<div class="week-grid">
<div
v-for="day in weekDaysData"
:key="day.date"
class="week-day"
>
<div class="week-day-header" :class="{ 'is-today': day.isToday }">
<div class="weekday">{{ day.weekday }}</div>
<div class="day-number">{{ day.dayNumber }}</div>
</div>
<div class="week-day-content">
<div
v-for="item in day.schedules"
:key="item.id"
class="schedule-item"
@click="showScheduleDetail(item)"
>
<div class="schedule-time">{{ item.scheduledTime }}</div>
<div class="schedule-info">
<div class="schedule-class">{{ item.className }}</div>
<div class="schedule-lesson">{{ item.courseName }}</div>
</div>
</div>
<div v-if="day.schedules.length === 0" class="no-schedule">无排课</div>
</div>
</div>
</div>
</div>
<!-- 日排课详情弹窗 -->
<a-modal
v-model:open="dayDetailVisible"
:title="selectedDateLabel"
@cancel="dayDetailVisible = false"
:footer="null"
width="500px"
>
<div class="day-schedules">
<div
v-for="item in selectedDaySchedules"
:key="item.id"
class="day-schedule-item"
>
<div class="item-time">{{ item.scheduledTime || '待定' }}</div>
<div class="item-info">
<div class="item-class">{{ item.className }}</div>
<div class="item-lesson">{{ item.courseName }}</div>
<div class="item-teacher">{{ item.teacherName || '未分配' }}</div>
</div>
<div class="item-status">
<a-tag :color="item.status === 'ACTIVE' || item.status === 'scheduled' ? 'success' : 'default'">
{{ item.status === 'ACTIVE' || item.status === 'scheduled' ? '有效' : '已取消' }}
</a-tag>
</div>
</div>
<a-empty v-if="selectedDaySchedules.length === 0" description="当日无排课" />
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import {
getCalendarViewData,
getClasses,
getTeachers,
type ClassInfo,
type CalendarViewResponse,
type DayScheduleItem,
} from '@/api/school';
const viewType = ref<'month' | 'week'>('month');
const selectedClassId = ref<number | undefined>();
const selectedTeacherId = ref<number | undefined>();
const classes = ref<ClassInfo[]>([]);
const teachers = ref<any[]>([]);
// 当前日期范围
const currentStartDate = ref(dayjs().startOf('month'));
const currentEndDate = ref(dayjs().endOf('month'));
// 日历数据
const calendarData = ref<CalendarViewResponse | null>(null);
// 选中日期详情
const dayDetailVisible = ref(false);
const selectedDate = ref<string>('');
const selectedDaySchedules = ref<DayScheduleItem[]>([]);
// 星期标题
const weekdayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
// 计算属性:当前范围标签
const currentRangeLabel = computed(() => {
if (viewType.value === 'month') {
return currentStartDate.value.format('YYYY年MM月');
} else {
const start = currentStartDate.value.format('MM-DD');
const end = currentEndDate.value.format('MM-DD');
return `${start} ~ ${end}`;
}
});
// 计算属性:选中日期标签
const selectedDateLabel = computed(() => {
if (!selectedDate.value) return '';
return dayjs(selectedDate.value).format('YYYY年MM月DD日');
});
// 计算属性:月日历天数
const calendarDays = computed(() => {
const days: any[] = [];
const startDate = currentStartDate.value.startOf('month');
const endDate = currentEndDate.value.endOf('month');
const today = dayjs().format('YYYY-MM-DD');
// 填充月初空白
const startWeekday = startDate.day();
for (let i = 0; i < startWeekday; i++) {
const date = startDate.subtract(startWeekday - i, 'day');
days.push({
key: `prev-${i}`,
date: date.format('YYYY-MM-DD'),
dayNumber: date.date(),
isOtherMonth: true,
isToday: date.format('YYYY-MM-DD') === today,
hasSchedule: hasScheduleOnDate(date.format('YYYY-MM-DD')),
scheduleCount: getScheduleCount(date.format('YYYY-MM-DD')),
});
}
// 当月日期
for (let d = dayjs(startDate); d.isBefore(endDate.add(1, 'day')); d = d.add(1, 'day')) {
days.push({
key: d.format('YYYY-MM-DD'),
date: d.format('YYYY-MM-DD'),
dayNumber: d.date(),
isOtherMonth: false,
isToday: d.format('YYYY-MM-DD') === today,
hasSchedule: hasScheduleOnDate(d.format('YYYY-MM-DD')),
scheduleCount: getScheduleCount(d.format('YYYY-MM-DD')),
});
}
// 填充月末空白保持6行
const remainingDays = 42 - days.length;
for (let i = 0; i < remainingDays; i++) {
const date = endDate.add(1, 'day').add(i, 'day');
days.push({
key: `next-${i}`,
date: date.format('YYYY-MM-DD'),
dayNumber: date.date(),
isOtherMonth: true,
isToday: date.format('YYYY-MM-DD') === today,
hasSchedule: hasScheduleOnDate(date.format('YYYY-MM-DD')),
scheduleCount: getScheduleCount(date.format('YYYY-MM-DD')),
});
}
return days;
});
// 计算属性:周日历天数
const weekDaysData = computed(() => {
const days: any[] = [];
const startDate = currentStartDate.value;
const today = dayjs().format('YYYY-MM-DD');
for (let i = 0; i < 7; i++) {
const date = startDate.add(i, 'day');
const dateStr = date.format('YYYY-MM-DD');
days.push({
date: dateStr,
weekday: weekdayNames[date.day()],
dayNumber: date.date(),
isToday: dateStr === today,
schedules: getScheduleForDate(dateStr),
});
}
return days;
});
// 检查某日期是否有排课
const hasScheduleOnDate = (dateStr: string): boolean => {
if (!calendarData.value?.schedules) return false;
const schedules = calendarData.value.schedules[dateStr];
return schedules && schedules.length > 0;
};
// 获取某日期的排课数量
const getScheduleCount = (dateStr: string): number => {
if (!calendarData.value?.schedules) return 0;
const schedules = calendarData.value.schedules[dateStr];
return schedules ? schedules.length : 0;
};
// 获取某日期的排课列表
const getScheduleForDate = (dateStr: string): DayScheduleItem[] => {
if (!calendarData.value?.schedules) return [];
return calendarData.value.schedules[dateStr] || [];
};
// 加载日历数据
const loadEvents = async () => {
try {
const params: any = {
startDate: currentStartDate.value.format('YYYY-MM-DD'),
endDate: currentEndDate.value.format('YYYY-MM-DD'),
};
if (selectedClassId.value) {
params.classId = selectedClassId.value;
}
if (selectedTeacherId.value) {
params.teacherId = selectedTeacherId.value;
}
calendarData.value = await getCalendarViewData(params);
} catch (error) {
console.error('Failed to load events:', error);
message.error('加载日历数据失败');
}
};
// 加载基础数据
const loadClasses = async () => {
try {
classes.value = await getClasses();
} catch (error) {
console.error('Failed to load classes:', error);
}
};
const loadTeachers = async () => {
try {
const data = await getTeachers({});
teachers.value = data.list || [];
} catch (error) {
console.error('Failed to load teachers:', error);
}
};
// 导航操作
const prevNav = () => {
if (viewType.value === 'month') {
currentStartDate.value = currentStartDate.value.subtract(1, 'month');
currentEndDate.value = currentEndDate.value.subtract(1, 'month');
} else {
currentStartDate.value = currentStartDate.value.subtract(1, 'week');
currentEndDate.value = currentEndDate.value.subtract(1, 'week');
}
loadEvents();
};
const nextNav = () => {
if (viewType.value === 'month') {
currentStartDate.value = currentStartDate.value.add(1, 'month');
currentEndDate.value = currentEndDate.value.add(1, 'month');
} else {
currentStartDate.value = currentStartDate.value.add(1, 'week');
currentEndDate.value = currentEndDate.value.add(1, 'week');
}
loadEvents();
};
// 点击日期
const handleDayClick = (day: any) => {
selectedDate.value = day.date;
selectedDaySchedules.value = getScheduleForDate(day.date);
dayDetailVisible.value = true;
};
// 显示排课详情(周视图)
const showScheduleDetail = (item: DayScheduleItem) => {
selectedDaySchedules.value = [item];
selectedDate.value = ''; // 周视图不需要显示具体日期
dayDetailVisible.value = true;
};
// 监听视图类型变化
watch(viewType, (newType) => {
if (newType === 'month') {
currentStartDate.value = dayjs().startOf('month');
currentEndDate.value = dayjs().endOf('month');
} else {
currentStartDate.value = dayjs().startOf('week').add(1, 'day');
currentEndDate.value = currentStartDate.value.add(6, 'day');
}
loadEvents();
});
// 暴露刷新方法给父组件
defineExpose({
refresh: loadEvents,
});
onMounted(() => {
loadClasses();
loadTeachers();
loadEvents();
});
</script>
<style scoped lang="scss">
.calendar-view {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.current-range-label {
font-weight: 500;
color: #333;
min-width: 120px;
text-align: center;
}
}
}
// 月视图
.calendar-month {
.weekday-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: #F0F0F0;
margin-bottom: 8px;
.weekday-item {
padding: 12px;
text-align: center;
font-weight: 500;
color: #666;
}
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: #E0E0E0;
border: 1px solid #E0E0E0;
}
.calendar-day {
background: white;
min-height: 80px;
padding: 8px;
cursor: pointer;
transition: background 0.2s;
position: relative;
&:hover {
background: #FFF8F0;
}
&.today {
background: #FFF0E6;
}
&.other-month {
color: #CCC;
background: #FAFAFA;
}
.day-number {
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.schedule-indicator {
width: 6px;
height: 6px;
background: #FF8C42;
border-radius: 50%;
margin: 0 auto;
}
.schedule-count {
position: absolute;
top: 4px;
right: 4px;
background: #FF8C42;
color: white;
font-size: 10px;
padding: 2px 4px;
border-radius: 8px;
min-width: 16px;
text-align: center;
}
}
}
// 周视图
.calendar-week {
.week-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
}
.week-day {
border: 1px solid #E0E0E0;
border-radius: 8px;
overflow: hidden;
}
.week-day-header {
padding: 12px;
background: #FFF8F0;
text-align: center;
border-bottom: 1px solid #E0E0E0;
&.is-today {
background: #FFE8CC;
}
.weekday {
font-size: 12px;
color: #999;
}
.day-number {
font-size: 20px;
font-weight: 600;
color: #2D3436;
}
}
.week-day-content {
padding: 8px;
min-height: 300px;
max-height: 400px;
overflow-y: auto;
}
.schedule-item {
padding: 8px;
margin-bottom: 6px;
background: #FFF0E6;
border-left: 3px solid #FF8C42;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #FFE0D0;
transform: translateX(2px);
}
.schedule-time {
font-size: 12px;
color: #FF8C42;
font-weight: 500;
}
.schedule-info {
margin-top: 4px;
.schedule-class {
font-size: 13px;
color: #2D3436;
}
.schedule-lesson {
font-size: 11px;
color: #999;
}
}
}
.no-schedule {
text-align: center;
color: #CCC;
font-size: 12px;
margin-top: 20px;
}
}
// 日详情弹窗
.day-schedules {
.day-schedule-item {
display: flex;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background: #FAFAFA;
border-radius: 8px;
.item-time {
min-width: 100px;
font-weight: 500;
color: #FF8C42;
}
.item-info {
flex: 1;
.item-class {
font-size: 14px;
color: #2D3436;
}
.item-lesson {
font-size: 12px;
color: #999;
}
.item-teacher {
font-size: 12px;
color: #666;
}
}
.item-status {
margin-left: 12px;
}
}
}
</style>