library-picturebook-activity/frontend/src/views/homework/StudentList.vue

496 lines
13 KiB
Vue
Raw Normal View History

<template>
<div class="student-homework-page">
<a-card class="mb-4">
<template #title>我的作业</template>
</a-card>
<!-- 搜索表单 -->
<a-form
:model="searchParams"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item label="作业名称">
<a-input
v-model:value="searchParams.name"
placeholder="请输入作业名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</a-form-item>
</a-form>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
{{ record.name }}
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.submission ? 'success' : 'warning'">
{{ record.submission ? "已提交" : "待提交" }}
</a-tag>
</template>
<template v-else-if="column.key === 'submitTime'">
<div>
{{ formatDateTime(record.submitStartTime) }} ~
{{ formatDateTime(record.submitEndTime) }}
</div>
</template>
<template v-else-if="column.key === 'action'">
<a-button type="link" size="small" @click="handleViewDetail(record)">
详情
</a-button>
</template>
</template>
</a-table>
<!-- 作业详情侧边弹框 -->
<a-drawer
v-model:open="detailModalVisible"
title="作业详情"
placement="right"
width="500px"
destroy-on-close
:footer-style="{ textAlign: 'right' }"
>
<a-spin :spinning="detailLoading">
<!-- 作业信息 -->
<div class="section-title">作业信息</div>
<a-descriptions :column="1" size="small" bordered class="mb-4">
<a-descriptions-item label="作业名称">
{{ currentHomework?.name }}
</a-descriptions-item>
<a-descriptions-item label="提交时间">
{{ formatDateTime(currentHomework?.submitStartTime) }} ~
{{ formatDateTime(currentHomework?.submitEndTime) }}
</a-descriptions-item>
<a-descriptions-item label="作业描述">
<div class="homework-content" v-html="formatContent(currentHomework?.content)"></div>
</a-descriptions-item>
<a-descriptions-item label="附件下载" v-if="homeworkAttachments.length > 0">
<div class="attachment-list">
<a
v-for="(att, index) in homeworkAttachments"
:key="index"
:href="att.fileUrl"
target="_blank"
class="attachment-item"
>
<DownloadOutlined />
{{ att.fileName }}
</a>
</div>
</a-descriptions-item>
</a-descriptions>
<!-- 作业提交 -->
<div class="section-title">作业提交</div>
<a-result
v-if="isExpired && !currentSubmission"
status="warning"
title="提交已截止"
sub-title="很抱歉,作业提交时间已过"
/>
<a-form
v-else
ref="formRef"
:model="submitForm"
:rules="currentSubmission ? {} : formRules"
layout="vertical"
>
<a-form-item label="作品名称" name="workName">
<a-input
v-model:value="submitForm.workName"
placeholder="请输入作品名称"
:disabled="!!currentSubmission"
/>
</a-form-item>
<a-form-item label="作品介绍" name="workDescription">
<a-textarea
v-model:value="submitForm.workDescription"
placeholder="请输入作品介绍"
:rows="4"
:disabled="!!currentSubmission"
/>
</a-form-item>
<a-form-item label="上传作品" name="files">
<template v-if="currentSubmission">
<!-- 已提交显示已上传的文件 -->
<div class="file-list" v-if="submissionFiles.length > 0">
<a
v-for="(file, index) in submissionFiles"
:key="index"
:href="file.fileUrl"
target="_blank"
class="file-item"
>
<FileOutlined />
{{ file.fileName }}
</a>
</div>
<span v-else class="text-gray">暂无附件</span>
</template>
<template v-else>
<!-- 未提交显示上传组件 -->
<a-upload
v-model:file-list="submitForm.fileList"
:before-upload="beforeUpload"
:custom-request="customUpload"
>
<a-button>
<template #icon><UploadOutlined /></template>
上传附件
</a-button>
</a-upload>
</template>
</a-form-item>
<!-- 已提交显示得分 -->
<a-form-item v-if="currentSubmission && currentSubmission.totalScore !== null && currentSubmission.totalScore !== undefined" label="得分">
<a-tag color="blue" style="font-size: 16px;">
{{ currentSubmission.totalScore }}
</a-tag>
</a-form-item>
</a-form>
</a-spin>
<!-- 底部按钮 -->
<template #footer>
<a-space>
<a-button @click="detailModalVisible = false">取消</a-button>
<a-button
v-if="!currentSubmission && !isExpired"
type="primary"
:loading="submitting"
@click="handleSubmit"
>
确定提交
</a-button>
</a-space>
</template>
</a-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from "vue"
import { useRoute } from "vue-router"
import { message } from "ant-design-vue"
import type { FormInstance, UploadProps } from "ant-design-vue"
import {
SearchOutlined,
ReloadOutlined,
DownloadOutlined,
FileOutlined,
UploadOutlined,
} from "@ant-design/icons-vue"
import { useListRequest } from "@/composables/useListRequest"
import {
homeworksApi,
submissionsApi,
type Homework,
type QueryHomeworkParams,
type HomeworkSubmission,
type HomeworkAttachment,
} from "@/api/homework"
import { uploadApi } from "@/api/upload"
import dayjs from "dayjs"
const route = useRoute()
// 使用列表请求组合函数 - 只获取已发布的作业
const {
loading,
dataSource,
pagination,
searchParams,
fetchList,
resetSearch,
search,
handleTableChange,
} = useListRequest<Homework, QueryHomeworkParams>({
requestFn: (params) => homeworksApi.getMyList({ ...params, status: 'published' }),
defaultSearchParams: {} as QueryHomeworkParams,
defaultPageSize: 10,
errorMessage: "获取作业列表失败",
})
// 表格列定义
const columns = [
{
title: "序号",
key: "index",
width: 60,
customRender: ({ index }: { index: number }) => index + 1,
},
{
title: "作业名称",
key: "name",
dataIndex: "name",
width: 250,
},
{
title: "状态",
key: "status",
width: 100,
},
{
title: "提交时间",
key: "submitTime",
width: 350,
},
{
title: "操作",
key: "action",
width: 100,
fixed: "right" as const,
},
]
// 格式化日期时间
const formatDateTime = (dateStr?: string) => {
if (!dateStr) return "-"
return dayjs(dateStr).format("YYYY-MM-DD HH:mm:ss")
}
// 格式化内容(保留换行)
const formatContent = (content?: string) => {
if (!content) return ""
return content.replace(/\n/g, "<br/>")
}
// 搜索
const handleSearch = () => {
search()
}
// 重置搜索
const handleReset = () => {
resetSearch()
}
// =============== 详情弹框相关 ===============
const detailModalVisible = ref(false)
const detailLoading = ref(false)
const currentHomework = ref<Homework | null>(null)
const currentSubmission = ref<HomeworkSubmission | null>(null)
const submitting = ref(false)
const formRef = ref<FormInstance>()
// 解析作业附件
const homeworkAttachments = computed<HomeworkAttachment[]>(() => {
if (!currentHomework.value?.attachments) return []
if (typeof currentHomework.value.attachments === "string") {
try {
return JSON.parse(currentHomework.value.attachments)
} catch {
return []
}
}
return currentHomework.value.attachments as HomeworkAttachment[]
})
// 解析提交的文件
const submissionFiles = computed<HomeworkAttachment[]>(() => {
if (!currentSubmission.value?.files) return []
if (typeof currentSubmission.value.files === "string") {
try {
return JSON.parse(currentSubmission.value.files)
} catch {
return []
}
}
return currentSubmission.value.files as HomeworkAttachment[]
})
// 是否过期
const isExpired = computed(() => {
if (!currentHomework.value) return false
return dayjs().isAfter(dayjs(currentHomework.value.submitEndTime))
})
// 提交表单
const submitForm = reactive<{
workName: string
workDescription: string
fileList: any[]
}>({
workName: "",
workDescription: "",
fileList: [],
})
const formRules = {
workName: [{ required: true, message: "请输入作品名称" }],
}
// 查看详情
const handleViewDetail = async (record: Homework) => {
// 重置状态
currentHomework.value = null
currentSubmission.value = null
detailLoading.value = true
detailModalVisible.value = true
// 重置表单
submitForm.workName = ""
submitForm.workDescription = ""
submitForm.fileList = []
// 加载作业详情
try {
const detail = await homeworksApi.getDetail(record.id)
currentHomework.value = detail
} catch (error: any) {
message.error(error?.response?.data?.message || "获取作业详情失败")
detailLoading.value = false
return
}
// 获取我的提交记录404 是正常情况,不提示错误)
try {
const mySubmission = await submissionsApi.getMySubmission(record.id)
currentSubmission.value = mySubmission
} catch {
// 未找到提交记录是正常情况,不提示错误
currentSubmission.value = null
}
detailLoading.value = false
}
// 文件上传
const beforeUpload: UploadProps["beforeUpload"] = () => {
return false
}
const customUpload = async ({ file, onSuccess, onError }: any) => {
try {
const formData = new FormData()
formData.append("file", file)
const result = await uploadApi.upload(formData, "homework/attachment")
file.url = result.url
onSuccess(result)
} catch (error) {
onError(error)
}
}
// 提交作业
const handleSubmit = async () => {
if (!currentHomework.value) return
try {
await formRef.value?.validate()
submitting.value = true
const files = submitForm.fileList.map((file) => ({
fileName: file.name,
fileUrl: file.url || file.response?.url,
size: file.size?.toString(),
}))
await submissionsApi.submit({
homeworkId: currentHomework.value.id,
workName: submitForm.workName,
workDescription: submitForm.workDescription,
files,
})
message.success("提交成功")
detailModalVisible.value = false
// 刷新列表
fetchList()
} catch (error: any) {
if (error?.errorFields) return
message.error(error?.response?.data?.message || "提交失败")
} finally {
submitting.value = false
}
}
onMounted(() => {
fetchList()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.mb-4 {
margin-bottom: 16px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
padding-left: 8px;
border-left: 3px solid #1890ff;
}
.homework-content {
white-space: pre-wrap;
line-height: 1.6;
color: #666;
}
.attachment-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.attachment-item {
display: flex;
align-items: center;
gap: 6px;
color: #1890ff;
cursor: pointer;
}
.attachment-item:hover {
text-decoration: underline;
}
.file-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.file-item {
display: flex;
align-items: center;
gap: 6px;
color: #1890ff;
cursor: pointer;
}
.file-item:hover {
text-decoration: underline;
}
</style>