373 lines
12 KiB
Vue
373 lines
12 KiB
Vue
<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>
|