kindergarten/reading-platform-frontend/src/views/school/schedule/ScheduleView.vue

965 lines
30 KiB
Vue
Raw Normal View History

<template>
<div class="schedule-view">
<div class="page-header">
<h2>课程排期</h2>
<a-space>
<a-dropdown>
<a-button>
<template #icon><PlusOutlined /></template>
新建排课
<DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="handleCreateMenuClick">
<a-menu-item key="single">
<PlusOutlined /> 单个新建
</a-menu-item>
<a-menu-item key="batch">
<AppstoreAddOutlined /> 批量新建
</a-menu-item>
<a-menu-item key="template">
<CopyOutlined /> 从模板创建
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-button @click="showTemplateModal">
<template #icon><CopyOutlined /></template>
排课模板
</a-button>
<a-dropdown>
<a-button>
<template #icon><CalendarOutlined /></template>
视图切换
<DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="handleViewMenuClick">
<a-menu-item key="list">
<UnorderedListOutlined /> 列表视图
</a-menu-item>
<a-menu-item key="timetable">
<TableOutlined /> 课表视图
</a-menu-item>
<a-menu-item key="calendar">
<CalendarOutlined /> 日历视图
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</div>
<!-- 筛选区 -->
<div class="filter-section">
<a-space wrap>
<a-select
v-model:value="filters.classId"
placeholder="选择班级"
allowClear
style="width: 150px"
@change="loadSchedules"
>
<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="loadSchedules"
>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
</a-select>
<a-range-picker
:value="dateRange"
@change="handleDateChange"
/>
<a-select
v-model:value="filters.status"
placeholder="状态"
allowClear
style="width: 120px"
@change="loadSchedules"
>
<a-select-option value="ACTIVE">有效</a-select-option>
<a-select-option value="CANCELLED">已取消</a-select-option>
</a-select>
</a-space>
</div>
<!-- 排课列表 -->
<a-table
:columns="columns"
:data-source="schedules"
:loading="loading"
:pagination="pagination"
rowKey="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'scheduledDate'">
{{ formatDate(record.scheduledDate) }}
<span v-if="record.scheduledTime" class="time-slot">{{ record.scheduledTime }}</span>
</template>
<template v-if="column.key === 'repeatType'">
<a-tag v-if="record.repeatType === 'NONE'" color="default">单次</a-tag>
<a-tag v-else-if="record.repeatType === 'DAILY'" color="blue">每日</a-tag>
<a-tag v-else-if="record.repeatType === 'WEEKLY'" color="green">每周</a-tag>
</template>
<template v-if="column.key === 'source'">
<a-tag v-if="record.source === 'SCHOOL'" color="orange">学校排课</a-tag>
<a-tag v-else color="purple">教师预约</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 'ACTIVE'" color="success">有效</a-tag>
<a-tag v-else color="error">已取消</a-tag>
</template>
<template v-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="showEditModal(record)">编辑</a-button>
<a-popconfirm
v-if="record.status === 'ACTIVE'"
title="确定要取消此排课吗?"
@confirm="handleCancel(record.id)"
>
<a-button type="link" size="small" danger>取消</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 新建/编辑排课弹窗 -->
<a-modal
v-model:open="modalVisible"
:title="editingSchedule ? '编辑排课' : '新建排课'"
:confirm-loading="modalLoading"
@ok="handleSubmit"
@cancel="handleModalCancel"
width="600px"
>
<a-form
ref="formRef"
:model="formState"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="班级" name="classId">
<a-select
v-model:value="formState.classId"
placeholder="选择班级"
:disabled="!!editingSchedule"
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="课程" name="courseId">
<a-select
v-model:value="formState.courseId"
placeholder="选择课程"
:disabled="!!editingSchedule"
>
<a-select-option v-for="course in courses" :key="course.id" :value="course.id">
{{ course.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="授课教师" name="teacherId">
<a-select
v-model:value="formState.teacherId"
placeholder="选择教师(可选)"
allowClear
>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="排课日期" name="scheduledDate">
<a-date-picker v-model:value="formState.scheduledDate" style="width: 100%" />
</a-form-item>
<a-form-item label="时间段" name="scheduledTimeRange">
<a-time-range-picker
v-model:value="formState.scheduledTimeRange"
format="HH:mm"
style="width: 100%"
:placeholder="['开始时间', '结束时间']"
/>
</a-form-item>
<a-form-item label="重复方式" name="repeatType">
<a-radio-group v-model:value="formState.repeatType">
<a-radio value="NONE">单次</a-radio>
<a-radio value="DAILY">每日重复</a-radio>
<a-radio value="WEEKLY">每周重复</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item v-if="formState.repeatType !== 'NONE'" label="重复截止" name="repeatEndDate">
<a-date-picker v-model:value="formState.repeatEndDate" style="width: 100%" />
</a-form-item>
<a-form-item label="备注" name="note">
<a-textarea v-model:value="formState.note" :rows="2" placeholder="备注信息" />
</a-form-item>
</a-form>
</a-modal>
<!-- 排课模板管理弹窗 -->
<a-modal
v-model:open="templateModalVisible"
title="排课模板管理"
:footer="null"
width="800px"
>
<div class="template-header">
<a-button type="primary" size="small" @click="showCreateTemplateModal">
<PlusOutlined /> 新建模板
</a-button>
</div>
<a-table
:columns="templateColumns"
:data-source="templates"
:loading="templateLoading"
size="small"
rowKey="id"
:pagination="{ pageSize: 5 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'weekDay'">
{{ weekDayNames[record.weekDay] || '-' }}
</template>
<template v-if="column.key === 'isDefault'">
<a-tag v-if="record.isDefault" color="blue">默认</a-tag>
<span v-else>-</span>
</template>
<template v-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="applyTemplate(record as any)">应用</a-button>
<a-button type="link" size="small" @click="showEditTemplateModal(record as any)">编辑</a-button>
<a-popconfirm title="确定删除?" @confirm="handleDeleteTemplate((record as any).id)">
<a-button type="link" size="small" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-modal>
<!-- 新建/编辑模板弹窗 -->
<a-modal
v-model:open="templateFormModalVisible"
:title="editingTemplate ? '编辑模板' : '新建模板'"
:confirm-loading="templateFormLoading"
@ok="handleTemplateSubmit"
>
<a-form
:model="templateForm"
:rules="templateFormRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="模板名称" name="name">
<a-input v-model:value="templateForm.name" placeholder="如:周一早读" />
</a-form-item>
<a-form-item label="课程" name="courseId">
<a-select v-model:value="templateForm.courseId" placeholder="选择课程">
<a-select-option v-for="course in courses" :key="course.id" :value="course.id">
{{ course.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="班级" name="classId">
<a-select v-model:value="templateForm.classId" placeholder="选择班级" allowClear>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="教师" name="teacherId">
<a-select v-model:value="templateForm.teacherId" placeholder="选择教师" allowClear>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="时间段" name="scheduledTime">
<a-input v-model:value="templateForm.scheduledTime" placeholder="如09:00-09:30" />
</a-form-item>
<a-form-item label="周几" name="weekDay">
<a-select v-model:value="templateForm.weekDay" placeholder="选择周几" allowClear>
<a-select-option :value="1">周一</a-select-option>
<a-select-option :value="2">周二</a-select-option>
<a-select-option :value="3">周三</a-select-option>
<a-select-option :value="4">周四</a-select-option>
<a-select-option :value="5">周五</a-select-option>
<a-select-option :value="6">周六</a-select-option>
<a-select-option :value="0">周日</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="时长(分钟)" name="duration">
<a-input-number v-model:value="templateForm.duration" :min="5" :max="120" />
</a-form-item>
<a-form-item label="设为默认" name="isDefault">
<a-switch v-model:checked="templateForm.isDefault" />
</a-form-item>
</a-form>
</a-modal>
<!-- 批量排课弹窗 -->
<a-modal
v-model:open="batchModalVisible"
title="批量新建排课"
:confirm-loading="batchLoading"
@ok="handleBatchSubmit"
width="900px"
>
<a-alert
message="批量添加排课信息,点击下方按钮添加更多行"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<div class="batch-header">
<a-button type="dashed" @click="addBatchItem">
<PlusOutlined /> 添加排课
</a-button>
</div>
<a-table
:columns="batchColumns"
:data-source="batchItems"
size="small"
rowKey="key"
:pagination="false"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'classId'">
<a-select v-model:value="record.classId" placeholder="班级" style="width: 100%">
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
</a-select>
</template>
<template v-if="column.key === 'courseId'">
<a-select v-model:value="record.courseId" placeholder="课程" style="width: 100%">
<a-select-option v-for="course in courses" :key="course.id" :value="course.id">
{{ course.name }}
</a-select-option>
</a-select>
</template>
<template v-if="column.key === 'teacherId'">
<a-select v-model:value="record.teacherId" placeholder="教师" style="width: 100%" allowClear>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
</a-select>
</template>
<template v-if="column.key === 'scheduledDate'">
<a-date-picker v-model:value="record.scheduledDate" style="width: 100%" />
</template>
<template v-if="column.key === 'scheduledTime'">
<a-input v-model:value="record.scheduledTime" placeholder="09:00-09:30" style="width: 100%" />
</template>
<template v-if="column.key === 'actions'">
<a-button type="link" size="small" danger @click="removeBatchItem(index)">
<DeleteOutlined />
</a-button>
</template>
</template>
</a-table>
</a-modal>
<!-- 从模板创建弹窗 -->
<a-modal
v-model:open="templateSelectModalVisible"
title="从模板创建排课"
:confirm-loading="templateSelectLoading"
@ok="handleTemplateSelectSubmit"
>
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="选择模板">
<a-select v-model:value="selectedTemplateId" placeholder="选择排课模板" style="width: 100%">
<a-select-option v-for="tpl in templates" :key="tpl.id" :value="tpl.id">
{{ tpl.name }} - {{ tpl.courseName }} ({{ tpl.scheduledTime }})
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="排课日期">
<a-date-picker v-model:value="templateApplyDate" style="width: 100%" />
</a-form-item>
<a-form-item v-if="selectedTemplate" label="模板详情">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="课程">{{ selectedTemplate.courseName }}</a-descriptions-item>
<a-descriptions-item label="班级">{{ selectedTemplate.className || '未指定' }}</a-descriptions-item>
<a-descriptions-item label="教师">{{ selectedTemplate.teacherName || '未指定' }}</a-descriptions-item>
<a-descriptions-item label="时间">{{ selectedTemplate.scheduledTime }}</a-descriptions-item>
</a-descriptions>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import type { TableProps, FormInstance } from 'ant-design-vue';
import dayjs, { Dayjs } from 'dayjs';
import { PlusOutlined, CalendarOutlined, DownOutlined, AppstoreAddOutlined, CopyOutlined, DeleteOutlined, UnorderedListOutlined, TableOutlined } from '@ant-design/icons-vue';
import {
getSchedules,
createSchedule,
updateSchedule,
cancelSchedule,
getClasses,
getTeachers,
getSchoolCourses,
getScheduleTemplates,
createScheduleTemplate,
updateScheduleTemplate,
deleteScheduleTemplate,
applyScheduleTemplate,
batchCreateSchedules,
type SchedulePlan,
type CreateScheduleDto,
type UpdateScheduleDto,
type ClassInfo,
type Teacher,
type ScheduleTemplate,
} from '@/api/school';
const router = useRouter();
// 数据
const loading = ref(false);
const schedules = ref<SchedulePlan[]>([]);
const classes = ref<ClassInfo[]>([]);
const teachers = ref<Teacher[]>([]);
const courses = ref<any[]>([]);
// 筛选
const filters = reactive({
classId: undefined as number | undefined,
teacherId: undefined as number | undefined,
status: undefined as string | undefined,
startDate: undefined as string | undefined,
endDate: undefined as string | undefined,
});
const dateRange = ref<[Dayjs, Dayjs] | undefined>(undefined);
// 分页
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showTotal: (total: number) => `${total}`,
});
// 表格列
const columns = [
{ title: '班级', dataIndex: 'className', key: 'className' },
{ title: '课程', dataIndex: 'courseName', key: 'courseName' },
{ title: '授课教师', dataIndex: 'teacherName', key: 'teacherName' },
{ title: '排课时间', key: 'scheduledDate' },
{ title: '重复', dataIndex: 'repeatType', key: 'repeatType' },
{ title: '来源', dataIndex: 'source', key: 'source' },
{ title: '状态', dataIndex: 'status', key: 'status' },
{ title: '操作', key: 'actions', width: 150 },
];
// 弹窗
const modalVisible = ref(false);
const modalLoading = ref(false);
const editingSchedule = ref<SchedulePlan | null>(null);
const formRef = ref<FormInstance>();
// 表单
const formState = reactive<CreateScheduleDto & { repeatEndDate?: Dayjs; scheduledDate?: Dayjs; scheduledTimeRange?: [Dayjs, Dayjs] }>({
classId: undefined as any,
courseId: undefined as any,
teacherId: undefined,
scheduledDate: undefined,
scheduledTimeRange: undefined,
repeatType: 'NONE',
repeatEndDate: undefined,
note: undefined,
});
const formRules = {
classId: [{ required: true, message: '请选择班级' }],
courseId: [{ required: true, message: '请选择课程' }],
scheduledDate: [{ required: true, message: '请选择排课日期' }],
repeatType: [{ required: true, message: '请选择重复方式' }],
};
// 加载数据
const loadSchedules = async () => {
loading.value = true;
try {
const res = await getSchedules({
page: pagination.current,
pageSize: pagination.pageSize,
...filters,
});
schedules.value = res.items;
pagination.total = res.total;
} catch (error) {
message.error('加载排课列表失败');
} finally {
loading.value = false;
}
};
const loadBaseData = async () => {
try {
const [classesRes, teachersRes, coursesRes] = await Promise.all([
getClasses(),
getTeachers({ page: 1, pageSize: 100 }),
getSchoolCourses(),
]);
classes.value = classesRes;
teachers.value = teachersRes.items;
courses.value = coursesRes;
} catch (error) {
message.error('加载基础数据失败');
}
};
// 事件处理
const handleTableChange: TableProps['onChange'] = (pag) => {
pagination.current = pag.current || 1;
pagination.pageSize = pag.pageSize || 20;
loadSchedules();
};
const handleDateChange = (dates: any) => {
if (dates && dates.length === 2) {
dateRange.value = dates;
filters.startDate = dates[0].format('YYYY-MM-DD');
filters.endDate = dates[1].format('YYYY-MM-DD');
} else {
dateRange.value = undefined;
filters.startDate = undefined;
filters.endDate = undefined;
}
loadSchedules();
};
const showCreateModal = () => {
editingSchedule.value = null;
Object.assign(formState, {
classId: undefined,
courseId: undefined,
teacherId: undefined,
scheduledDate: undefined,
scheduledTimeRange: undefined,
repeatType: 'NONE',
repeatEndDate: undefined,
note: undefined,
});
modalVisible.value = true;
};
const showEditModal = (record: any) => {
editingSchedule.value = record;
// 解析时间字符串为时间范围
let timeRange: [Dayjs, Dayjs] | undefined = undefined;
if (record.scheduledTime) {
const parts = record.scheduledTime.split('-');
if (parts.length === 2) {
const baseDate = dayjs().format('YYYY-MM-DD');
timeRange = [
dayjs(`${baseDate} ${parts[0]}`, 'YYYY-MM-DD HH:mm'),
dayjs(`${baseDate} ${parts[1]}`, 'YYYY-MM-DD HH:mm'),
];
}
}
Object.assign(formState, {
classId: record.classId,
courseId: record.courseId,
teacherId: record.teacherId,
scheduledDate: record.scheduledDate ? dayjs(record.scheduledDate) : undefined,
scheduledTimeRange: timeRange,
repeatType: record.repeatType,
repeatEndDate: record.repeatEndDate ? dayjs(record.repeatEndDate) : undefined,
note: record.note,
});
modalVisible.value = true;
};
const handleModalCancel = () => {
modalVisible.value = false;
formRef.value?.resetFields();
};
const handleSubmit = async () => {
try {
await formRef.value?.validate();
modalLoading.value = true;
// 格式化时间范围
let scheduledTime: string | undefined = undefined;
if (formState.scheduledTimeRange && formState.scheduledTimeRange.length === 2) {
scheduledTime = `${formState.scheduledTimeRange[0].format('HH:mm')}-${formState.scheduledTimeRange[1].format('HH:mm')}`;
}
const data: CreateScheduleDto | UpdateScheduleDto = {
classId: formState.classId,
courseId: formState.courseId,
teacherId: formState.teacherId,
scheduledDate: formState.scheduledDate?.format('YYYY-MM-DD'),
scheduledTime,
repeatType: formState.repeatType,
repeatEndDate: formState.repeatEndDate?.format('YYYY-MM-DD'),
note: formState.note,
};
if (editingSchedule.value) {
await updateSchedule(editingSchedule.value.id, data);
message.success('更新成功');
} else {
await createSchedule(data as CreateScheduleDto);
message.success('创建成功');
}
modalVisible.value = false;
loadSchedules();
} catch (error) {
message.error(editingSchedule.value ? '更新失败' : '创建失败');
} finally {
modalLoading.value = false;
}
};
const handleCancel = async (id: number) => {
try {
await cancelSchedule(id);
message.success('取消成功');
loadSchedules();
} catch (error) {
message.error('取消失败');
}
};
const switchToTimetable = () => {
router.push('/school/schedule/timetable');
};
const switchToCalendar = () => {
router.push('/school/schedule/calendar');
};
// 视图切换菜单点击处理
const handleViewMenuClick = (e: any) => {
const key = e.key;
if (key === 'list') {
// 当前页面就是列表视图
} else if (key === 'timetable') {
switchToTimetable();
} else if (key === 'calendar') {
switchToCalendar();
}
};
const formatDate = (date: string | undefined) => {
if (!date) return '-';
return dayjs(date).format('YYYY-MM-DD');
};
// ==================== 模板管理 ====================
const templateModalVisible = ref(false);
const templateLoading = ref(false);
const templates = ref<ScheduleTemplate[]>([]);
const templateColumns = [
{ title: '模板名称', dataIndex: 'name', key: 'name' },
{ title: '课程', dataIndex: 'courseName', key: 'courseName' },
{ title: '班级', dataIndex: 'className', key: 'className' },
{ title: '教师', dataIndex: 'teacherName', key: 'teacherName' },
{ title: '时间', dataIndex: 'scheduledTime', key: 'scheduledTime' },
{ title: '周几', key: 'weekDay' },
{ title: '默认', key: 'isDefault' },
{ title: '操作', key: 'actions', width: 180 },
];
const weekDayNames: Record<number, string> = {
0: '周日',
1: '周一',
2: '周二',
3: '周三',
4: '周四',
5: '周五',
6: '周六',
};
const showTemplateModal = async () => {
templateModalVisible.value = true;
await loadTemplates();
};
const loadTemplates = async () => {
templateLoading.value = true;
try {
templates.value = await getScheduleTemplates();
} catch (error) {
message.error('加载模板失败');
} finally {
templateLoading.value = false;
}
};
// 模板表单
const templateFormModalVisible = ref(false);
const templateFormLoading = ref(false);
const editingTemplate = ref<ScheduleTemplate | null>(null);
const templateForm = reactive({
name: '',
courseId: undefined as number | undefined,
classId: undefined as number | undefined,
teacherId: undefined as number | undefined,
scheduledTime: '09:00-09:30',
weekDay: undefined as number | undefined,
duration: 30,
isDefault: false,
});
const templateFormRules = {
name: [{ required: true, message: '请输入模板名称' }],
courseId: [{ required: true, message: '请选择课程' }],
};
const showCreateTemplateModal = () => {
editingTemplate.value = null;
Object.assign(templateForm, {
name: '',
courseId: undefined,
classId: undefined,
teacherId: undefined,
scheduledTime: '09:00-09:30',
weekDay: undefined,
duration: 30,
isDefault: false,
});
templateFormModalVisible.value = true;
};
const showEditTemplateModal = (record: ScheduleTemplate) => {
editingTemplate.value = record;
Object.assign(templateForm, {
name: record.name,
courseId: record.courseId,
classId: record.classId,
teacherId: record.teacherId,
scheduledTime: record.scheduledTime || '09:00-09:30',
weekDay: record.weekDay,
duration: record.duration || 30,
isDefault: record.isDefault,
});
templateFormModalVisible.value = true;
};
const handleTemplateSubmit = async () => {
templateFormLoading.value = true;
try {
if (editingTemplate.value) {
await updateScheduleTemplate(editingTemplate.value.id, templateForm as any);
message.success('更新成功');
} else {
await createScheduleTemplate(templateForm as any);
message.success('创建成功');
}
templateFormModalVisible.value = false;
await loadTemplates();
} catch (error) {
message.error(editingTemplate.value ? '更新失败' : '创建失败');
} finally {
templateFormLoading.value = false;
}
};
const handleDeleteTemplate = async (id: number) => {
try {
await deleteScheduleTemplate(id);
message.success('删除成功');
await loadTemplates();
} catch (error) {
message.error('删除失败');
}
};
// 从模板应用
const templateSelectModalVisible = ref(false);
const templateSelectLoading = ref(false);
const selectedTemplateId = ref<number | undefined>();
const templateApplyDate = ref<Dayjs>(dayjs());
const selectedTemplate = computed(() => {
if (!selectedTemplateId.value) return null;
return templates.value.find(t => t.id === selectedTemplateId.value);
});
const applyTemplate = (record: ScheduleTemplate) => {
selectedTemplateId.value = record.id;
templateApplyDate.value = dayjs();
templateSelectModalVisible.value = true;
};
const handleTemplateSelectSubmit = async () => {
if (!selectedTemplateId.value || !templateApplyDate.value) {
message.warning('请选择模板和日期');
return;
}
templateSelectLoading.value = true;
try {
await applyScheduleTemplate(selectedTemplateId.value, {
scheduledDate: templateApplyDate.value.format('YYYY-MM-DD'),
});
message.success('应用模板成功');
templateSelectModalVisible.value = false;
templateModalVisible.value = false;
await loadSchedules();
} catch (error) {
message.error('应用模板失败');
} finally {
templateSelectLoading.value = false;
}
};
// ==================== 批量排课 ====================
const batchModalVisible = ref(false);
const batchLoading = ref(false);
const batchItems = ref<any[]>([]);
let batchKey = 0;
const batchColumns = [
{ title: '班级', key: 'classId', width: 120 },
{ title: '课程', key: 'courseId', width: 150 },
{ title: '教师', key: 'teacherId', width: 100 },
{ title: '日期', key: 'scheduledDate', width: 140 },
{ title: '时间', key: 'scheduledTime', width: 120 },
{ title: '', key: 'actions', width: 50 },
];
const showBatchModal = () => {
batchItems.value = [];
batchKey = 0;
addBatchItem();
batchModalVisible.value = true;
};
const addBatchItem = () => {
batchItems.value.push({
key: ++batchKey,
classId: undefined,
courseId: undefined,
teacherId: undefined,
scheduledDate: dayjs(),
scheduledTime: '09:00-09:30',
});
};
const removeBatchItem = (index: number) => {
batchItems.value.splice(index, 1);
};
const handleBatchSubmit = async () => {
// 验证数据
const validItems = batchItems.value.filter(item =>
item.classId && item.courseId && item.scheduledDate && item.scheduledTime
);
if (validItems.length === 0) {
message.warning('请至少填写一条完整的排课信息');
return;
}
batchLoading.value = true;
try {
const schedules = validItems.map(item => ({
classId: item.classId,
courseId: item.courseId,
teacherId: item.teacherId,
scheduledDate: item.scheduledDate.format('YYYY-MM-DD'),
scheduledTime: item.scheduledTime,
}));
const result = await batchCreateSchedules(schedules);
message.success(`成功创建 ${result.success} 条排课${result.failed > 0 ? `${result.failed} 条失败` : ''}`);
batchModalVisible.value = false;
await loadSchedules();
} catch (error) {
message.error('批量创建失败');
} finally {
batchLoading.value = false;
}
};
// 创建菜单点击处理
const handleCreateMenuClick = (e: any) => {
const key = e.key;
if (key === 'single') {
showCreateModal();
} else if (key === 'batch') {
showBatchModal();
} else if (key === 'template') {
selectedTemplateId.value = undefined;
templateApplyDate.value = dayjs();
templateSelectModalVisible.value = true;
loadTemplates();
}
};
onMounted(() => {
loadBaseData();
loadSchedules();
});
</script>
<style scoped lang="scss">
.schedule-view {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
}
}
.filter-section {
margin-bottom: 20px;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.time-slot {
margin-left: 8px;
color: #666;
font-size: 12px;
}
.template-header {
margin-bottom: 16px;
display: flex;
justify-content: flex-end;
}
.batch-header {
margin-bottom: 16px;
}
}
</style>