课表优化
This commit is contained in:
@@ -28,6 +28,22 @@ export async function downloadApiFile(
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export async function downloadApiPostFile(
|
||||
path: string,
|
||||
payload: unknown,
|
||||
fallbackName: string,
|
||||
) {
|
||||
const response = await http.post(path, payload, { responseType: 'blob' })
|
||||
const url = URL.createObjectURL(response.data)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = responseFileName(response.headers['content-disposition'], fallbackName)
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
anchor.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export async function importExcel(path: string, file: File) {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
|
||||
+131
-30
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { downloadApiFile } from '../api/excel'
|
||||
import { downloadApiFile, downloadApiPostFile } from '../api/excel'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
|
||||
|
||||
@@ -32,6 +32,8 @@ const isManager = computed(() =>
|
||||
)
|
||||
const loading = ref(false)
|
||||
const exportingPdf = ref(false)
|
||||
const batchExporting = ref(false)
|
||||
const batchResourceIds = ref<string[]>([])
|
||||
const calendarDialogVisible = ref(false)
|
||||
const calendarLoading = ref(false)
|
||||
const calendarActionLoading = ref(false)
|
||||
@@ -61,7 +63,6 @@ const viewMode = ref<'overview' | 'week' | 'day'>('week')
|
||||
const selectedWeek = ref(1)
|
||||
const selectedDay = ref(1)
|
||||
const loadedTermId = ref('')
|
||||
const exportArea = ref<HTMLElement | null>(null)
|
||||
const weekdays = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||
const patternLabels: Record<string, string> = { All: '每周', Odd: '单周', Even: '双周' }
|
||||
const planStatusLabels: Record<string, string> = {
|
||||
@@ -104,6 +105,11 @@ const selectedResourceId = computed(() => {
|
||||
if (resourceType.value === 'Classroom') return classroomId.value
|
||||
return classId.value
|
||||
})
|
||||
const batchResources = computed(() => {
|
||||
if (resourceType.value === 'Teacher') return filteredTeachers.value
|
||||
if (resourceType.value === 'Classroom') return filteredClassrooms.value
|
||||
return filteredClasses.value
|
||||
})
|
||||
const slotMap = computed<Map<number, any>>(() =>
|
||||
new Map<number, any>(
|
||||
(timetable.value?.slots ?? []).map((item: any) => [item.periodNumber, item]),
|
||||
@@ -444,6 +450,7 @@ async function loadManagementOptions() {
|
||||
}
|
||||
|
||||
async function onTermChanged() {
|
||||
batchResourceIds.value = []
|
||||
if (isManager.value) await loadManagementOptions()
|
||||
await loadTimetable()
|
||||
}
|
||||
@@ -472,6 +479,7 @@ function onBuildingChanged() {
|
||||
}
|
||||
|
||||
function onResourceTypeChanged() {
|
||||
batchResourceIds.value = []
|
||||
collegeId.value = ''
|
||||
majorId.value = ''
|
||||
grade.value = undefined
|
||||
@@ -487,6 +495,67 @@ function onResourceTypeChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
function resourceLabel(item: any) {
|
||||
if (resourceType.value === 'Teacher') return `${item.teacherNumber} · ${item.name}`
|
||||
if (resourceType.value === 'Classroom') return `${item.buildingName} · ${item.name}`
|
||||
return `${item.code} · ${item.name}`
|
||||
}
|
||||
|
||||
function selectFilteredResources() {
|
||||
if (batchResources.value.length > 100) {
|
||||
ElMessage.warning('单次最多导出 100 项,请进一步缩小筛选范围。')
|
||||
return
|
||||
}
|
||||
batchResourceIds.value = batchResources.value.map((item: any) => item.id)
|
||||
}
|
||||
|
||||
function batchExportPayload() {
|
||||
if (!batchResourceIds.value.length) {
|
||||
ElMessage.warning('请先从筛选结果中选择要导出的课表。')
|
||||
return null
|
||||
}
|
||||
return {
|
||||
resourceType: resourceType.value,
|
||||
resourceIds: batchResourceIds.value,
|
||||
academicTermId: termId.value,
|
||||
schedulePlanId: planId.value || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
async function exportBatchExcel() {
|
||||
const payload = batchExportPayload()
|
||||
if (!payload) return
|
||||
batchExporting.value = true
|
||||
try {
|
||||
await downloadApiPostFile(
|
||||
'/timetables/management/export/batch.xlsx',
|
||||
payload,
|
||||
`课表批量导出-${terms.value.find((item: any) => item.id === termId.value)?.name ?? ''}.xlsx`,
|
||||
)
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
batchExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function exportBatchPdf() {
|
||||
const payload = batchExportPayload()
|
||||
if (!payload) return
|
||||
batchExporting.value = true
|
||||
try {
|
||||
await downloadApiPostFile(
|
||||
'/timetables/management/export/batch.pdf',
|
||||
payload,
|
||||
`课表批量导出-${terms.value.find((item: any) => item.id === termId.value)?.name ?? ''}.pdf`,
|
||||
)
|
||||
} catch (error) {
|
||||
ElMessage.error(`PDF 导出失败:${apiErrorMessage(error)}`)
|
||||
} finally {
|
||||
batchExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTimetable() {
|
||||
if (!termId.value) {
|
||||
timetable.value = null
|
||||
@@ -564,35 +633,23 @@ async function exportExcel() {
|
||||
}
|
||||
|
||||
async function exportPdf() {
|
||||
if (!exportArea.value || !timetable.value) return
|
||||
exportingPdf.value = true
|
||||
try {
|
||||
await nextTick()
|
||||
const [{ default: html2canvas }, { jsPDF }] = await Promise.all([
|
||||
import('html2canvas'),
|
||||
import('jspdf'),
|
||||
])
|
||||
const canvas = await html2canvas(exportArea.value, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
backgroundColor: '#ffffff',
|
||||
})
|
||||
const pdf = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' })
|
||||
const pageWidth = 297
|
||||
const pageHeight = 210
|
||||
const imageHeight = canvas.height * pageWidth / canvas.width
|
||||
const image = canvas.toDataURL('image/png')
|
||||
let remaining = imageHeight
|
||||
let position = 0
|
||||
pdf.addImage(image, 'PNG', 0, position, pageWidth, imageHeight)
|
||||
remaining -= pageHeight
|
||||
while (remaining > 0) {
|
||||
position = remaining - imageHeight
|
||||
pdf.addPage()
|
||||
pdf.addImage(image, 'PNG', 0, position, pageWidth, imageHeight)
|
||||
remaining -= pageHeight
|
||||
if (isMine.value) {
|
||||
await downloadApiFile(`/timetables/mine/export.pdf?academicTermId=${termId.value}`, '我的课表.pdf')
|
||||
} else if (isTeacherView.value && teacherIdParam.value) {
|
||||
await downloadApiFile(`/timetables/teachers/${teacherIdParam.value}/export.pdf?academicTermId=${termId.value}`, '教师课表.pdf')
|
||||
} else if (isManager.value) {
|
||||
const query = new URLSearchParams({
|
||||
resourceType: resourceType.value,
|
||||
resourceId: selectedResourceId.value,
|
||||
academicTermId: termId.value,
|
||||
})
|
||||
if (planId.value) query.set('schedulePlanId', planId.value)
|
||||
await downloadApiFile(`/timetables/management/export.pdf?${query}`, '课表.pdf')
|
||||
} else {
|
||||
await downloadApiFile(`/timetables/classes/${classId.value}/export.pdf?academicTermId=${termId.value}`, '班级课表.pdf')
|
||||
}
|
||||
pdf.save(`${timetable.value.subject?.name ?? '课表'}-${timetable.value.term.name}.pdf`)
|
||||
} catch (error) {
|
||||
ElMessage.error(`PDF 导出失败:${apiErrorMessage(error)}`)
|
||||
} finally {
|
||||
@@ -853,6 +910,46 @@ onMounted(async () => {
|
||||
{{ resourceType === 'Class' ? filteredClasses.length : resourceType === 'Teacher' ? filteredTeachers.length : filteredClassrooms.length }}
|
||||
项
|
||||
</small>
|
||||
<div v-if="isManager" class="batch-export-panel">
|
||||
<div class="batch-export-heading">
|
||||
<strong>批量导出</strong>
|
||||
<span>按当前筛选结果多选,最多 100 项</span>
|
||||
</div>
|
||||
<el-select
|
||||
v-model="batchResourceIds"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="选择要批量导出的课表"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in batchResources"
|
||||
:key="item.id"
|
||||
:label="resourceLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="batch-export-actions">
|
||||
<el-button size="small" @click="selectFilteredResources">全选筛选结果</el-button>
|
||||
<el-button size="small" :disabled="!batchResourceIds.length" @click="batchResourceIds = []">清空</el-button>
|
||||
<span>已选 {{ batchResourceIds.length }} 项</span>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="batchExporting"
|
||||
:disabled="!batchResourceIds.length"
|
||||
@click="exportBatchExcel"
|
||||
>批量导出 Excel</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:loading="batchExporting"
|
||||
:disabled="!batchResourceIds.length"
|
||||
@click="exportBatchPdf"
|
||||
>批量导出 PDF</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-loading="loading" class="timetable-sheet">
|
||||
@@ -910,7 +1007,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="timetable" ref="exportArea" class="timetable-export-area">
|
||||
<div v-if="timetable" class="timetable-export-area">
|
||||
<div class="sheet-meta">
|
||||
<div>
|
||||
<strong>{{ timetable.subject.name }}</strong>
|
||||
@@ -1290,6 +1387,10 @@ onMounted(async () => {
|
||||
.calendar-coverage ul { margin: 10px 0; padding-left: 20px; color: #526b7d; line-height: 1.8; font-size: 13px; }
|
||||
.calendar-coverage p { margin: 0; color: #788895; font-size: 12px; line-height: 1.7; }
|
||||
.timetable-export-area { min-width: 0; padding: 2px; background: #fff; }
|
||||
.batch-export-panel { display: grid; gap: 10px; width: min(760px, 100%); margin-top: 8px; padding: 14px; border: 1px solid #d7e3ea; border-radius: 8px; background: #f8fbfc; }
|
||||
.batch-export-heading { display: flex; align-items: baseline; gap: 10px; color: #17324d; }
|
||||
.batch-export-heading span, .batch-export-actions span { color: #718191; font-size: 12px; }
|
||||
.batch-export-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.sheet-meta { display: flex; justify-content: space-between; gap: 24px; margin-bottom: 18px; padding-bottom: 16px; border-bottom: 1px solid #e6ebf0; }
|
||||
.sheet-meta div { display: grid; gap: 3px; }
|
||||
.sheet-meta strong { color: #17324d; }
|
||||
|
||||
Reference in New Issue
Block a user