kindergarten_java/reading-platform-frontend/src/views/school/schedule/CalendarView.vue

594 lines
15 KiB
Vue
Raw Normal View History

<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>