353 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			353 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"
 | 
						||
        >
 | 
						||
          <svg
 | 
						||
            class="w-6 h-6"
 | 
						||
            fill="none"
 | 
						||
            stroke="currentColor"
 | 
						||
            viewBox="0 0 24 24"
 | 
						||
          >
 | 
						||
            <path
 | 
						||
              stroke-linecap="round"
 | 
						||
              stroke-linejoin="round"
 | 
						||
              stroke-width="2"
 | 
						||
              d="M6 18L18 6M6 6l12 12"
 | 
						||
            />
 | 
						||
          </svg>
 | 
						||
        </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">未完成</div>
 | 
						||
                  </div>
 | 
						||
 | 
						||
                  <div class="flex justify-between items-center">
 | 
						||
                    <span class="text-base-content/70">批阅状态</span>
 | 
						||
                    <div class="badge badge-ghost">待提交</div>
 | 
						||
                  </div>
 | 
						||
 | 
						||
                  <div class="flex justify-between items-center">
 | 
						||
                    <span class="text-base-content/70">成绩</span>
 | 
						||
                    <span class="text-base-content/50">未评分</span>
 | 
						||
                  </div>
 | 
						||
                </div>
 | 
						||
 | 
						||
                <div class="divider"></div>
 | 
						||
 | 
						||
                <!-- 提交历史 -->
 | 
						||
                <div class="space-y-3">
 | 
						||
                  <h4 class="font-medium text-base-content">提交历史</h4>
 | 
						||
                  <div
 | 
						||
                    v-if="isUndefined(commitsList)"
 | 
						||
                    class="text-sm text-base-content/50 text-center py-4"
 | 
						||
                  >
 | 
						||
                    暂无提交记录
 | 
						||
                  </div>
 | 
						||
                  <div v-else class="overflow-y-auto">
 | 
						||
                    <ul class="steps steps-vertical">
 | 
						||
                      <li class="step step-primary">Register</li>
 | 
						||
                      <li class="step step-primary">Choose plan</li>
 | 
						||
                      <li class="step">Purchase</li>
 | 
						||
                      <li class="step">Receive Product</li>
 | 
						||
                    </ul>
 | 
						||
                  </div>
 | 
						||
                </div>
 | 
						||
              </div>
 | 
						||
            </div>
 | 
						||
 | 
						||
            <!-- 操作按钮 -->
 | 
						||
            <div class="space-y-3">
 | 
						||
              <button @click="startExam" class="btn btn-primary w-full">
 | 
						||
                <svg
 | 
						||
                  class="w-5 h-5 mr-2"
 | 
						||
                  fill="none"
 | 
						||
                  stroke="currentColor"
 | 
						||
                  viewBox="0 0 24 24"
 | 
						||
                >
 | 
						||
                  <path
 | 
						||
                    stroke-linecap="round"
 | 
						||
                    stroke-linejoin="round"
 | 
						||
                    stroke-width="2"
 | 
						||
                    d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
 | 
						||
                  />
 | 
						||
                </svg>
 | 
						||
                开始实验
 | 
						||
              </button>
 | 
						||
 | 
						||
              <button
 | 
						||
                @click="downloadResources"
 | 
						||
                class="btn btn-outline w-full"
 | 
						||
                :disabled="downloadingResources"
 | 
						||
              >
 | 
						||
                <svg
 | 
						||
                  class="w-5 h-5 mr-2"
 | 
						||
                  fill="none"
 | 
						||
                  stroke="currentColor"
 | 
						||
                  viewBox="0 0 24 24"
 | 
						||
                >
 | 
						||
                  <path
 | 
						||
                    stroke-linecap="round"
 | 
						||
                    stroke-linejoin="round"
 | 
						||
                    stroke-width="2"
 | 
						||
                    d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
 | 
						||
                  />
 | 
						||
                </svg>
 | 
						||
                <span v-if="downloadingResources">下载中...</span>
 | 
						||
                <span v-else>下载资源包</span>
 | 
						||
              </button>
 | 
						||
 | 
						||
              <button class="btn btn-outline w-full">
 | 
						||
                <svg
 | 
						||
                  class="w-5 h-5 mr-2"
 | 
						||
                  fill="none"
 | 
						||
                  stroke="currentColor"
 | 
						||
                  viewBox="0 0 24 24"
 | 
						||
                >
 | 
						||
                  <path
 | 
						||
                    stroke-linecap="round"
 | 
						||
                    stroke-linejoin="round"
 | 
						||
                    stroke-width="2"
 | 
						||
                    d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
 | 
						||
                  />
 | 
						||
                </svg>
 | 
						||
                查看记录
 | 
						||
              </button>
 | 
						||
            </div>
 | 
						||
          </div>
 | 
						||
        </div>
 | 
						||
      </div>
 | 
						||
    </div>
 | 
						||
    <div class="modal-backdrop" @click="closeExamDetail"></div>
 | 
						||
  </div>
 | 
						||
</template>
 | 
						||
<script setup lang="ts">
 | 
						||
import type { Commit, ExamInfo } 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 { isNull, isUndefined } from "lodash";
 | 
						||
 | 
						||
const alertStore = useRequiredInjection(useAlertStore);
 | 
						||
const router = useRouter();
 | 
						||
 | 
						||
const show = defineModel<boolean>("show", {
 | 
						||
  default: false,
 | 
						||
});
 | 
						||
 | 
						||
const props = defineProps<{
 | 
						||
  selectedExam: ExamInfo;
 | 
						||
}>();
 | 
						||
 | 
						||
const commitsList = ref<Commit[]>();
 | 
						||
async function updateCommits() {
 | 
						||
  const client = AuthManager.createAuthenticatedExamClient();
 | 
						||
  const list = await client.getCommitsByExamId(props.selectedExam.id);
 | 
						||
  commitsList.value = list;
 | 
						||
}
 | 
						||
watch(() => props.selectedExam, updateCommits);
 | 
						||
 | 
						||
// Download resources
 | 
						||
const downloadingResources = ref(false);
 | 
						||
const downloadResources = async () => {
 | 
						||
  if (!props.selectedExam || downloadingResources.value) return;
 | 
						||
 | 
						||
  downloadingResources.value = true;
 | 
						||
 | 
						||
  try {
 | 
						||
    const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
						||
 | 
						||
    // 获取资源包列表(模板资源)
 | 
						||
    const resourceList = await resourceClient.getResourceList(
 | 
						||
      props.selectedExam.id,
 | 
						||
      "resource",
 | 
						||
      "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;
 | 
						||
  }
 | 
						||
};
 | 
						||
 | 
						||
// 开始实验
 | 
						||
const startExam = () => {
 | 
						||
  if (props.selectedExam) {
 | 
						||
    // 跳转到项目页面,传递实验ID
 | 
						||
    console.log("开始实验:", props.selectedExam.id);
 | 
						||
    router.push({
 | 
						||
      name: "project",
 | 
						||
      query: { examId: props.selectedExam.id },
 | 
						||
    });
 | 
						||
  }
 | 
						||
};
 | 
						||
 | 
						||
const closeExamDetail = () => {
 | 
						||
  show.value = false;
 | 
						||
};
 | 
						||
</script>
 | 
						||
 | 
						||
<style lang="postcss" scoped></style>
 |