2026-02-28 17:51:15 +08:00
|
|
|
|
<template>
|
2026-03-03 13:59:02 +08:00
|
|
|
|
<div>
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="flex flex-wrap gap-3 md:flex-nowrap md:justify-between md:items-center items-start mb-5"
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 class="m-0 flex-shrink-0">课程排期</h2>
|
|
|
|
|
|
<a-space :wrap="true">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<a-dropdown>
|
|
|
|
|
|
<a-button>
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<template #icon>
|
|
|
|
|
|
<PlusOutlined />
|
|
|
|
|
|
</template>
|
2026-02-28 17:51:15 +08:00
|
|
|
|
新建排课
|
|
|
|
|
|
<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">
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<template #icon>
|
|
|
|
|
|
<CopyOutlined />
|
|
|
|
|
|
</template>
|
2026-02-28 17:51:15 +08:00
|
|
|
|
排课模板
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-dropdown>
|
|
|
|
|
|
<a-button>
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<template #icon>
|
|
|
|
|
|
<CalendarOutlined />
|
|
|
|
|
|
</template>
|
2026-02-28 17:51:15 +08:00
|
|
|
|
视图切换
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 筛选区 -->
|
2026-03-03 13:59:02 +08:00
|
|
|
|
<div class="mb-5 p-4 bg-[#fafafa] rounded-lg">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<a-space wrap>
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<a-select v-model:value="filters.classId" placeholder="选择班级" allowClear class="w-[150px]"
|
|
|
|
|
|
@change="loadSchedules">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
|
|
|
|
|
|
{{ cls.name }}
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
</a-select>
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<a-select v-model:value="filters.teacherId" placeholder="选择教师" allowClear class="w-[150px]"
|
|
|
|
|
|
@change="loadSchedules">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
|
|
|
|
|
|
{{ teacher.name }}
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
</a-select>
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<a-range-picker :value="dateRange" @change="handleDateChange" />
|
|
|
|
|
|
<a-select v-model:value="filters.status" placeholder="状态" allowClear class="w-[120px]" @change="loadSchedules">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<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"
|
2026-03-03 17:38:29 +08:00
|
|
|
|
:scroll="{ x: true }"
|
2026-02-28 17:51:15 +08:00
|
|
|
|
@change="handleTableChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.key === 'scheduledDate'">
|
|
|
|
|
|
{{ formatDate(record.scheduledDate) }}
|
2026-03-03 13:59:02 +08:00
|
|
|
|
<span v-if="record.scheduledTime" class="ml-2 text-[#666] text-xs">{{ record.scheduledTime }}</span>
|
2026-02-28 17:51:15 +08:00
|
|
|
|
</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>
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<a-popconfirm v-if="record.status === 'ACTIVE'" title="确定要取消此排课吗?" @confirm="handleCancel(record.id)">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<a-button type="link" size="small" danger>取消</a-button>
|
|
|
|
|
|
</a-popconfirm>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 新建/编辑排课弹窗 -->
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<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 }">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<a-form-item label="班级" name="classId">
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<a-select v-model:value="formState.classId" placeholder="选择班级" :disabled="!!editingSchedule">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<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">
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<a-select v-model:value="formState.courseId" placeholder="选择课程" :disabled="!!editingSchedule">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<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">
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<a-select v-model:value="formState.teacherId" placeholder="选择教师(可选)" allowClear>
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<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">
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<a-time-range-picker v-model:value="formState.scheduledTimeRange" format="HH:mm" style="width: 100%"
|
|
|
|
|
|
:placeholder="['开始时间', '结束时间']" />
|
2026-02-28 17:51:15 +08:00
|
|
|
|
</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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 排课模板管理弹窗 -->
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<a-modal v-model:open="templateModalVisible" title="排课模板管理" :footer="null" width="800px">
|
2026-03-03 13:59:02 +08:00
|
|
|
|
<div class="mb-4 flex justify-end">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<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 }"
|
2026-03-03 17:38:29 +08:00
|
|
|
|
:scroll="{ x: true }"
|
2026-02-28 17:51:15 +08:00
|
|
|
|
>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 新建/编辑模板弹窗 -->
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<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 }">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 批量排课弹窗 -->
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<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" />
|
2026-03-03 13:59:02 +08:00
|
|
|
|
<div class="mb-4">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<a-button type="dashed" @click="addBatchItem">
|
|
|
|
|
|
<PlusOutlined /> 添加排课
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
:columns="batchColumns"
|
|
|
|
|
|
:data-source="batchItems"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
rowKey="key"
|
|
|
|
|
|
:pagination="false"
|
2026-03-03 17:38:29 +08:00
|
|
|
|
:scroll="{ x: true }"
|
2026-02-28 17:51:15 +08:00
|
|
|
|
>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 从模板创建弹窗 -->
|
2026-03-03 17:38:29 +08:00
|
|
|
|
<a-modal v-model:open="templateSelectModalVisible" title="从模板创建排课" :confirm-loading="templateSelectLoading"
|
|
|
|
|
|
@ok="handleTemplateSelectSubmit">
|
2026-02-28 17:51:15 +08:00
|
|
|
|
<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 = [
|
2026-03-03 17:38:29 +08:00
|
|
|
|
{ title: '班级', dataIndex: 'className', key: 'className', minWidth: 100 },
|
|
|
|
|
|
{ title: '课程', dataIndex: 'courseName', key: 'courseName', minWidth: 160 },
|
|
|
|
|
|
{ title: '授课教师', dataIndex: 'teacherName', key: 'teacherName', minWidth: 120 },
|
|
|
|
|
|
{ title: '排课时间', key: 'scheduledDate', minWidth: 180 },
|
|
|
|
|
|
{ title: '重复', dataIndex: 'repeatType', key: 'repeatType', minWidth: 80, maxWidth: 100 },
|
|
|
|
|
|
{ title: '来源', dataIndex: 'source', key: 'source', minWidth: 90, maxWidth: 110 },
|
|
|
|
|
|
{ title: '状态', dataIndex: 'status', key: 'status', minWidth: 80, maxWidth: 100 },
|
|
|
|
|
|
{ title: '操作', key: 'actions', minWidth: 150, fixed: 'right' as const },
|
2026-02-28 17:51:15 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 弹窗
|
|
|
|
|
|
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 = [
|
2026-03-03 17:38:29 +08:00
|
|
|
|
{ title: '模板名称', dataIndex: 'name', key: 'name', minWidth: 160 },
|
|
|
|
|
|
{ title: '课程', dataIndex: 'courseName', key: 'courseName', minWidth: 140 },
|
|
|
|
|
|
{ title: '班级', dataIndex: 'className', key: 'className', minWidth: 120 },
|
|
|
|
|
|
{ title: '教师', dataIndex: 'teacherName', key: 'teacherName', minWidth: 120 },
|
|
|
|
|
|
{ title: '时间', dataIndex: 'scheduledTime', key: 'scheduledTime', minWidth: 130, maxWidth: 150 },
|
|
|
|
|
|
{ title: '周几', key: 'weekDay', minWidth: 80, maxWidth: 90 },
|
|
|
|
|
|
{ title: '默认', key: 'isDefault', minWidth: 80, maxWidth: 90 },
|
|
|
|
|
|
{ title: '操作', key: 'actions', minWidth: 180, fixed: 'right' as const },
|
2026-02-28 17:51:15 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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 = [
|
2026-03-03 17:38:29 +08:00
|
|
|
|
{ title: '班级', key: 'classId', minWidth: 140, maxWidth: 160 },
|
|
|
|
|
|
{ title: '课程', key: 'courseId', minWidth: 170, maxWidth: 190 },
|
|
|
|
|
|
{ title: '教师', key: 'teacherId', minWidth: 120, maxWidth: 140 },
|
|
|
|
|
|
{ title: '日期', key: 'scheduledDate', minWidth: 150, maxWidth: 170 },
|
|
|
|
|
|
{ title: '时间', key: 'scheduledTime', minWidth: 130, maxWidth: 150 },
|
|
|
|
|
|
{ title: '操作', key: 'actions', minWidth: 70, maxWidth: 80, fixed: 'right' as const },
|
2026-02-28 17:51:15 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
|
2026-03-03 13:59:02 +08:00
|
|
|
|
<style scoped>
|
|
|
|
|
|
/* 仅保留无法用 UnoCSS 实现的部分 */
|
2026-03-03 17:38:29 +08:00
|
|
|
|
|
|
|
|
|
|
:deep(.ant-table-thead > tr > th) {
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
2026-02-28 17:51:15 +08:00
|
|
|
|
</style>
|