FPGA_WebLab/src/views/Exam/ExamInfoModal.vue

373 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div v-if="show" class="modal modal-open overflow-hidden">
<div
class="modal-box w-full max-w-6xl h-[90vh] max-h-[90vh] p-0 overflow-hidden"
>
<div
class="flex justify-between items-center p-6 border-b border-base-300"
>
<h2 class="text-2xl font-bold text-base-content">
{{ selectedExam.id }} - {{ selectedExam.name }}
</h2>
<button
@click="closeExamDetail"
class="btn btn-sm btn-circle btn-ghost"
>
<XIcon class="w-6 h-6" />
</button>
</div>
<div class="flex h-[calc(90vh-5rem)]">
<!-- 左侧:实验信息和描述 -->
<div class="flex-1 p-6 overflow-y-auto border-r border-base-300">
<div class="space-y-6">
<!-- 实验信息 -->
<div class="card bg-base-200">
<div class="card-body">
<h3 class="card-title text-lg mb-4">实验信息</h3>
<div class="space-y-3">
<div class="flex">
<span class="font-medium text-base-content w-24"
>实验ID</span
>
<span class="text-base-content/70">{{
selectedExam.id
}}</span>
</div>
<div class="flex">
<span class="font-medium text-base-content w-24"
>实验名称:</span
>
<span class="text-base-content/70">{{
selectedExam.name
}}</span>
</div>
<div class="flex">
<span class="font-medium text-base-content w-24"
>难度等级:</span
>
<div class="flex items-center gap-2">
<div class="rating rating-sm">
<span
v-for="i in 5"
:key="i"
class="mask mask-star-2"
:class="
i <= selectedExam.difficulty
? 'bg-orange-400'
: 'bg-base-300'
"
></span>
</div>
<span class="text-sm text-base-content/50"
>({{ selectedExam.difficulty }}/5)</span
>
</div>
</div>
<div
v-if="selectedExam.tags && selectedExam.tags.length > 0"
class="flex"
>
<span class="font-medium text-base-content w-24"
>标签:</span
>
<div class="flex flex-wrap gap-1">
<span
v-for="tag in selectedExam.tags"
:key="tag"
class="badge badge-outline badge-sm"
>{{ tag }}</span
>
</div>
</div>
<div class="flex">
<span class="font-medium text-base-content w-24"
>创建时间:</span
>
<span class="text-base-content/70">{{
formatDate(selectedExam.createdTime)
}}</span>
</div>
<div class="flex">
<span class="font-medium text-base-content w-24"
>更新时间:</span
>
<span class="text-base-content/70">{{
formatDate(selectedExam.updatedTime)
}}</span>
</div>
<div class="flex">
<span class="font-medium text-base-content w-24"
>可见性:</span
>
<span class="text-base-content/70">{{
selectedExam.isVisibleToUsers
? "对学生可见"
: "仅管理员可见"
}}</span>
</div>
</div>
</div>
</div>
<!-- 实验描述 -->
<div class="card bg-base-200">
<div class="card-body">
<h3 class="card-title text-lg mb-4">实验描述</h3>
<div class="prose prose-sm max-w-none">
<p class="text-base-content/70">
{{ selectedExam.description }}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧:完成情况和控制 -->
<div class="w-80 p-6 bg-base-200 overflow-y-auto">
<div class="space-y-6">
<!-- 完成情况 -->
<div class="card bg-base-100">
<div class="card-body">
<h3 class="card-title text-lg mb-4">完成情况</h3>
<div class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-base-content/70">当前状态</span>
<div class="badge badge-error">
{{
isUndefined(commitsList) || commitsList.length === 0
? "未提交"
: "已提交"
}}
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-base-content/70">成绩</span>
<div class="badge badge-ghost">未评分</div>
</div>
</div>
<div class="divider"></div>
<!-- 提交历史 -->
<div class="space-y-3">
<h4 class="font-medium text-base-content">提交历史</h4>
<div
v-if="isUndefined(commitsList) || commitsList.length === 0"
class="text-sm text-base-content/50 text-center py-4"
>
暂无提交记录
</div>
<div v-else class="overflow-y-auto fit-content max-h-50">
<ul class="steps steps-vertical">
<li
class="step"
:class="{
'step-primary': _idx === commitsList.length - 1,
}"
v-for="(commit, _idx) in commitsList"
>
{{ commit.uploadTime.toTimeString() }}
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="space-y-3">
<button @click="startExam" class="btn btn-primary w-full">
<Smile class="w-5 h-5" />
开始实验
</button>
<button @click="uploadModal?.show" class="btn btn-info w-full">
<Upload class="w-5 h-5" />
提交实验
</button>
<button
@click="downloadResources"
class="btn btn-outline w-full"
:disabled="downloadingResources"
>
<Download class="w-5 h-5" />
<span v-if="downloadingResources">下载中...</span>
<span v-else>下载资源包</span>
</button>
<button class="btn btn-outline w-full">
<GitGraph class="w-5 h-5" />
查看记录
</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-backdrop" @click="closeExamDetail"></div>
<UploadModal
ref="uploadModal"
class="fixed z-auto"
:auto-upload="true"
:close-after-upload="true"
:callback="submitExam"
@finished-upload="handleSubmitFinished"
/>
</div>
</template>
<script setup lang="ts">
import {
ExamClient,
ResourceClient,
ResourcePurpose,
type ExamInfo,
type ResourceInfo,
} from "@/APIClient";
import { useAlertStore } from "@/components/Alert";
import { AuthManager } from "@/utils/AuthManager";
import { useRequiredInjection } from "@/utils/Common";
import { defineModel, ref } from "vue";
import { useRouter } from "vue-router";
import { formatDate } from "@/utils/Common";
import { computed } from "vue";
import { watch } from "vue";
import { delay, isNull, isUndefined } from "lodash";
import { Download, GitGraph, Smile, Upload, XIcon } from "lucide-vue-next";
import UploadModal from "@/components/UploadModal.vue";
import { templateRef } from "@vueuse/core";
import { toFileParameter } from "@/utils/Common";
import { onMounted } from "vue";
const alertStore = useRequiredInjection(useAlertStore);
const router = useRouter();
const uploadModal = templateRef("uploadModal");
const show = defineModel<boolean>("show", {
default: false,
});
const props = defineProps<{
selectedExam: ExamInfo;
}>();
const commitsList = ref<ResourceInfo[]>();
async function updateCommits() {
const client = AuthManager.createClient(ExamClient);
const list = await client.getCommitsByExamId(props.selectedExam.id);
commitsList.value = list;
}
watch(
() => show.value,
() => {
if (show.value) {
updateCommits();
}
},
);
onMounted(() => {
if (show.value) {
updateCommits();
}
});
// Download resources
const downloadingResources = ref(false);
async function downloadResources() {
if (!props.selectedExam || downloadingResources.value) return;
downloadingResources.value = true;
try {
const resourceClient = AuthManager.createClient(ResourceClient);
// 获取资源包列表(模板资源)
const resourceList = await resourceClient.getResourceList(
props.selectedExam.id,
"resource",
ResourcePurpose.Template,
);
if (resourceList && resourceList.length > 0) {
// 使用新的ResourceClient API获取第一个资源包
const resourceId = resourceList[0].id;
const fileResponse = await resourceClient.getResourceById(resourceId);
// 创建Blob URL
const blobUrl = URL.createObjectURL(fileResponse.data);
// 创建下载链接
const link = document.createElement("a");
link.href = blobUrl;
link.download =
fileResponse.fileName ||
resourceList[0].name ||
`${props.selectedExam.name}_资源包`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 清理Blob URL
URL.revokeObjectURL(blobUrl);
alertStore.success("资料下载成功");
console.log("资料下载成功:", props.selectedExam.id);
} else {
alertStore.error("该实验暂无资料包");
}
} catch (err: any) {
alertStore.error(err.message || "下载资料失败");
console.error("下载资料失败:", err);
} finally {
downloadingResources.value = false;
}
}
// 开始实验
function startExam() {
if (props.selectedExam) {
// 跳转到项目页面传递实验ID
console.log("开始实验:", props.selectedExam.id);
router.push({
name: "project",
query: { examId: props.selectedExam.id },
});
}
}
function submitExam(files: File[]) {
try {
const client = AuthManager.createClient(ResourceClient);
for (const file of files) {
client.addResource(
"compression",
ResourcePurpose.Homework,
props.selectedExam.id,
toFileParameter(file),
);
}
} catch (err: any) {
alertStore.error(err.message || "上传资料失败");
console.error("上传资料失败:", err);
}
}
async function handleSubmitFinished() {
delay(async () => {
await updateCommits();
}, 1000);
}
function closeExamDetail() {
show.value = false;
}
</script>
<style lang="postcss" scoped></style>