|
@@ -53,6 +53,16 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="download-section">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ class="download-doc-button"
|
|
|
|
|
+ @click="openDownloadDialog"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-icon><Download /></el-icon>
|
|
|
|
|
+ 下载文本作业
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 右侧内容区域 -->
|
|
<!-- 右侧内容区域 -->
|
|
@@ -281,6 +291,68 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ v-model="downloadDialogVisible"
|
|
|
|
|
+ title="下载学生文本作业"
|
|
|
|
|
+ width="520px"
|
|
|
|
|
+ :close-on-click-modal="false"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-form label-position="top" class="download-form">
|
|
|
|
|
+ <el-form-item label="作业名称">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="downloadForm.assignmentIds"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ collapse-tags
|
|
|
|
|
+ collapse-tags-tooltip
|
|
|
|
|
+ filterable
|
|
|
|
|
+ placeholder="请选择作业名称"
|
|
|
|
|
+ class="download-form-select"
|
|
|
|
|
+ @change="handleDownloadAssignmentChange"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="assignment in downloadableAssignments"
|
|
|
|
|
+ :key="assignment.assignmentId"
|
|
|
|
|
+ :label="assignment.assignmentName"
|
|
|
|
|
+ :value="assignment.assignmentId"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="班级名称">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="downloadForm.classIds"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ collapse-tags
|
|
|
|
|
+ collapse-tags-tooltip
|
|
|
|
|
+ filterable
|
|
|
|
|
+ :placeholder="downloadClassPlaceholder"
|
|
|
|
|
+ class="download-form-select"
|
|
|
|
|
+ :disabled="downloadForm.assignmentIds.length === 0"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="classItem in downloadableClasses"
|
|
|
|
|
+ :key="classItem.classId"
|
|
|
|
|
+ :label="classItem.className"
|
|
|
|
|
+ :value="classItem.classId"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <div class="download-form-tip">
|
|
|
|
|
+ 将按“作业 x 班级”组合逐个下载 ZIP 文件。
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="closeDownloadDialog">取消</el-button>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ :loading="downloadSubmitting"
|
|
|
|
|
+ @click="handleBatchDownload"
|
|
|
|
|
+ >
|
|
|
|
|
+ 开始下载
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -288,9 +360,10 @@
|
|
|
import useApis from "@/apis";
|
|
import useApis from "@/apis";
|
|
|
import {ref, reactive, computed, onMounted, onUnmounted} from "vue";
|
|
import {ref, reactive, computed, onMounted, onUnmounted} from "vue";
|
|
|
import {useRouter, useRoute} from "vue-router";
|
|
import {useRouter, useRoute} from "vue-router";
|
|
|
- import {Refresh, Search, Document, ArrowDown, ArrowRight, User, CircleCheck, WarningFilled, Sort, ZoomIn} from "@element-plus/icons-vue";
|
|
|
|
|
|
|
+ import {Refresh, Search, Document, ArrowDown, ArrowRight, User, CircleCheck, WarningFilled, Sort, ZoomIn, Download} from "@element-plus/icons-vue";
|
|
|
import type {ClassDTO, StudentAssignmentStatusDTO} from "@/types/dto";
|
|
import type {ClassDTO, StudentAssignmentStatusDTO} from "@/types/dto";
|
|
|
import type {CourseVO, IAssignment} from "@/types/vo";
|
|
import type {CourseVO, IAssignment} from "@/types/vo";
|
|
|
|
|
+ import {ElMessage} from "element-plus";
|
|
|
import "./CoursePage.scss"
|
|
import "./CoursePage.scss"
|
|
|
import {useStore} from "@/store";
|
|
import {useStore} from "@/store";
|
|
|
const store = useStore()
|
|
const store = useStore()
|
|
@@ -299,6 +372,13 @@
|
|
|
|
|
|
|
|
const apis = useApis()
|
|
const apis = useApis()
|
|
|
const selectedAssignment = ref(null)
|
|
const selectedAssignment = ref(null)
|
|
|
|
|
+ const downloadDialogVisible = ref(false)
|
|
|
|
|
+ const downloadSubmitting = ref(false)
|
|
|
|
|
+ const downloadForm = reactive({
|
|
|
|
|
+ assignmentIds: [] as number[],
|
|
|
|
|
+ classIds: [] as number[]
|
|
|
|
|
+ })
|
|
|
|
|
+ const downloadClassCache = reactive<Record<number, ClassDTO[]>>({})
|
|
|
|
|
|
|
|
// 定义课程和作业的数据结构
|
|
// 定义课程和作业的数据结构
|
|
|
interface CourseWithAssignments {
|
|
interface CourseWithAssignments {
|
|
@@ -323,6 +403,39 @@
|
|
|
|
|
|
|
|
// 原始作业数据(从API获取)
|
|
// 原始作业数据(从API获取)
|
|
|
const rawAssignments = reactive<IAssignment[]>([])
|
|
const rawAssignments = reactive<IAssignment[]>([])
|
|
|
|
|
+
|
|
|
|
|
+ const downloadableAssignments = computed(() => {
|
|
|
|
|
+ return rawAssignments.filter(assignment => assignment.type === 'WRITING' || assignment.type === '写作')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const selectedDownloadAssignments = computed(() => {
|
|
|
|
|
+ return downloadableAssignments.value.filter(assignment => downloadForm.assignmentIds.includes(assignment.assignmentId))
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const downloadableClasses = computed(() => {
|
|
|
|
|
+ const classMap = new Map<number, ClassDTO>()
|
|
|
|
|
+
|
|
|
|
|
+ selectedDownloadAssignments.value.forEach(assignment => {
|
|
|
|
|
+ const classes = downloadClassCache[assignment.courseId] || []
|
|
|
|
|
+ classes.forEach(classItem => {
|
|
|
|
|
+ classMap.set(classItem.classId, classItem)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return Array.from(classMap.values())
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const downloadClassPlaceholder = computed(() => {
|
|
|
|
|
+ if (downloadForm.assignmentIds.length === 0) {
|
|
|
|
|
+ return '请先选择作业'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (downloadableClasses.value.length === 0) {
|
|
|
|
|
+ return '当前所选作业暂无可选班级'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return '请选择班级名称'
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
// 加载状态
|
|
// 加载状态
|
|
|
const loading = ref(false)
|
|
const loading = ref(false)
|
|
@@ -761,12 +874,154 @@
|
|
|
try {
|
|
try {
|
|
|
const response = await apis.getClassesByCourse(courseId)
|
|
const response = await apis.getClassesByCourse(courseId)
|
|
|
if (response.data.code === 200) {
|
|
if (response.data.code === 200) {
|
|
|
- classList.splice(0, classList.length, ...response.data.data)
|
|
|
|
|
|
|
+ const classes = response.data.data || []
|
|
|
|
|
+ classList.splice(0, classList.length, ...classes)
|
|
|
|
|
+ downloadClassCache[courseId] = classes
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载班级列表失败:', error)
|
|
console.error('加载班级列表失败:', error)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ const loadClassesForDownload = async (courseId: number) => {
|
|
|
|
|
+ if (downloadClassCache[courseId]) {
|
|
|
|
|
+ return downloadClassCache[courseId]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const response = await apis.getClassesByCourse(courseId)
|
|
|
|
|
+ if (response.data.code === 200) {
|
|
|
|
|
+ downloadClassCache[courseId] = response.data.data || []
|
|
|
|
|
+ return downloadClassCache[courseId]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ downloadClassCache[courseId] = []
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const openDownloadDialog = () => {
|
|
|
|
|
+ if (downloadableAssignments.value.length === 0) {
|
|
|
|
|
+ ElMessage.warning('暂无可下载的写作作业')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ downloadDialogVisible.value = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const closeDownloadDialog = () => {
|
|
|
|
|
+ downloadDialogVisible.value = false
|
|
|
|
|
+ downloadForm.assignmentIds = []
|
|
|
|
|
+ downloadForm.classIds = []
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleDownloadAssignmentChange = async () => {
|
|
|
|
|
+ const selectedCourseIds = Array.from(new Set(selectedDownloadAssignments.value.map(item => item.courseId)))
|
|
|
|
|
+
|
|
|
|
|
+ for (const courseId of selectedCourseIds) {
|
|
|
|
|
+ await loadClassesForDownload(courseId)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const validClassIds = new Set(downloadableClasses.value.map(item => item.classId))
|
|
|
|
|
+ downloadForm.classIds = downloadForm.classIds.filter(classId => validClassIds.has(classId))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const getHeaderValue = (headers: Record<string, any>, key: string) => {
|
|
|
|
|
+ const value = headers[key] ?? headers[key.toLowerCase()] ?? headers[key.toUpperCase()]
|
|
|
|
|
+ return typeof value === 'string' ? value : (value != null ? String(value) : '')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const getDownloadFilename = (headers: Record<string, any>, fallbackName: string) => {
|
|
|
|
|
+ const disposition = getHeaderValue(headers, 'content-disposition')
|
|
|
|
|
+ if (!disposition) {
|
|
|
|
|
+ return fallbackName
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i)
|
|
|
|
|
+ if (utf8Match?.[1]) {
|
|
|
|
|
+ return decodeURIComponent(utf8Match[1])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const plainMatch = disposition.match(/filename="?([^"]+)"?/i)
|
|
|
|
|
+ if (plainMatch?.[1]) {
|
|
|
|
|
+ return decodeURIComponent(plainMatch[1])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return fallbackName
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const triggerBrowserDownload = (blob: Blob, fileName: string) => {
|
|
|
|
|
+ const url = window.URL.createObjectURL(blob)
|
|
|
|
|
+ const link = document.createElement('a')
|
|
|
|
|
+ link.href = url
|
|
|
|
|
+ link.download = fileName
|
|
|
|
|
+ document.body.appendChild(link)
|
|
|
|
|
+ link.click()
|
|
|
|
|
+ document.body.removeChild(link)
|
|
|
|
|
+ window.URL.revokeObjectURL(url)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const parseBlobErrorMessage = async (blob: Blob) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const text = await blob.text()
|
|
|
|
|
+ const json = JSON.parse(text)
|
|
|
|
|
+ return json.msg || '下载失败'
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return '下载失败'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleBatchDownload = async () => {
|
|
|
|
|
+ if (downloadForm.assignmentIds.length === 0) {
|
|
|
|
|
+ ElMessage.warning('请先选择作业')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (downloadForm.classIds.length === 0) {
|
|
|
|
|
+ ElMessage.warning('请选择班级')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const selectedClasses = downloadableClasses.value.filter(classItem => downloadForm.classIds.includes(classItem.classId))
|
|
|
|
|
+ const downloadTasks = selectedDownloadAssignments.value.flatMap(assignment => {
|
|
|
|
|
+ return selectedClasses
|
|
|
|
|
+ .filter(classItem => classItem.courseId === assignment.courseId)
|
|
|
|
|
+ .map(classItem => ({ assignment, classItem }))
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (downloadTasks.length === 0) {
|
|
|
|
|
+ ElMessage.warning('所选作业与班级没有可下载的对应关系')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ downloadSubmitting.value = true
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ for (const task of downloadTasks) {
|
|
|
|
|
+ console.log('下载文本作业参数:', {
|
|
|
|
|
+ classId: task.classItem.classId,
|
|
|
|
|
+ assignmentId: task.assignment.assignmentId,
|
|
|
|
|
+ className: task.classItem.className,
|
|
|
|
|
+ assignmentName: task.assignment.assignmentName
|
|
|
|
|
+ })
|
|
|
|
|
+ const response = await apis.downloadClassAssignmentDocument(task.classItem.classId, task.assignment.assignmentId)
|
|
|
|
|
+ const contentType = getHeaderValue(response.headers as Record<string, any>, 'content-type')
|
|
|
|
|
+
|
|
|
|
|
+ if (contentType.includes('application/json')) {
|
|
|
|
|
+ throw new Error(await parseBlobErrorMessage(response.data))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const fallbackName = `${task.classItem.className}_${task.assignment.assignmentName}.zip`
|
|
|
|
|
+ const fileName = getDownloadFilename(response.headers as Record<string, any>, fallbackName)
|
|
|
|
|
+ triggerBrowserDownload(response.data, fileName)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ElMessage.success(`已开始下载 ${downloadTasks.length} 个文件`)
|
|
|
|
|
+ closeDownloadDialog()
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ console.error('批量下载文本作业失败:', error)
|
|
|
|
|
+ ElMessage.error(error?.message || '批量下载失败,请稍后重试')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ downloadSubmitting.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 加载学生作业状态
|
|
// 加载学生作业状态
|
|
|
const loadStudentAssignmentStatus = async (classId: string, assignmentId: string) => {
|
|
const loadStudentAssignmentStatus = async (classId: string, assignmentId: string) => {
|
|
@@ -1179,6 +1434,37 @@ export default {
|
|
|
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
|
|
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.download-section {
|
|
|
|
|
+ padding: 1rem 1.25rem 1.25rem;
|
|
|
|
|
+ border-top: 1px solid rgba(255, 255, 255, 0.18);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.download-doc-button {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.35);
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.16);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-family: "Noto Sans SC", sans-serif;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.download-doc-button:hover {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.24);
|
|
|
|
|
+ border-color: rgba(255, 255, 255, 0.55);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.download-form-select {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.download-form-tip {
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/* Webkit浏览器滚动条美化 */
|
|
/* Webkit浏览器滚动条美化 */
|
|
|
.menu-section::-webkit-scrollbar {
|
|
.menu-section::-webkit-scrollbar {
|
|
|
width: 6px;
|
|
width: 6px;
|