refactor: 重新调整exam页面
This commit is contained in:
		@@ -4,7 +4,7 @@ import AuthView from "../views/AuthView.vue";
 | 
			
		||||
import ProjectView from "../views/Project/Index.vue";
 | 
			
		||||
import TestView from "../views/TestView.vue";
 | 
			
		||||
import UserView from "@/views/User/Index.vue";
 | 
			
		||||
import ExamView from "@/views/ExamView.vue";
 | 
			
		||||
import ExamView from "@/views/Exam/Index.vue";
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
			
		||||
 
 | 
			
		||||
@@ -48,3 +48,14 @@ export function useOptionalInjection<T>(
 | 
			
		||||
  const value = useFn();
 | 
			
		||||
  return value ?? defaultValue;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatDate(date: Date | string) {
 | 
			
		||||
  const dateObj = typeof date === "string" ? new Date(date) : date;
 | 
			
		||||
  return dateObj.toLocaleString("zh-CN", {
 | 
			
		||||
    year: "numeric",
 | 
			
		||||
    month: "2-digit",
 | 
			
		||||
    day: "2-digit",
 | 
			
		||||
    hour: "2-digit",
 | 
			
		||||
    minute: "2-digit",
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										833
									
								
								src/views/Exam/ExamEditModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										833
									
								
								src/views/Exam/ExamEditModal.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,833 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="show" class="modal modal-open overflow-hidden">
 | 
			
		||||
    <div class="modal-box w-full max-w-7xl 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">创建新实验</h2>
 | 
			
		||||
        <button
 | 
			
		||||
          @click="closeCreateModal"
 | 
			
		||||
          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>
 | 
			
		||||
 | 
			
		||||
      <form @submit.prevent="submitCreateExam" class="flex h-[calc(90vh-5rem)]">
 | 
			
		||||
        <!-- 左侧:基本信息 -->
 | 
			
		||||
        <div class="w-110 p-6 overflow-y-auto border-r border-base-300">
 | 
			
		||||
          <div class="space-y-6">
 | 
			
		||||
            <h3 class="text-xl font-semibold text-base-content mb-4">
 | 
			
		||||
              基本信息
 | 
			
		||||
            </h3>
 | 
			
		||||
 | 
			
		||||
            <!-- 实验ID -->
 | 
			
		||||
            <div class="form-control">
 | 
			
		||||
              <label class="label">
 | 
			
		||||
                <span class="label-text font-medium">实验ID *</span>
 | 
			
		||||
              </label>
 | 
			
		||||
              <input
 | 
			
		||||
                type="text"
 | 
			
		||||
                v-model="newExam.id"
 | 
			
		||||
                class="input input-bordered w-full"
 | 
			
		||||
                placeholder="例如: EXP001"
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 实验名称 -->
 | 
			
		||||
            <div class="form-control">
 | 
			
		||||
              <label class="label">
 | 
			
		||||
                <span class="label-text font-medium">实验名称 *</span>
 | 
			
		||||
              </label>
 | 
			
		||||
              <input
 | 
			
		||||
                type="text"
 | 
			
		||||
                v-model="newExam.name"
 | 
			
		||||
                class="input input-bordered w-full"
 | 
			
		||||
                placeholder="实验名称"
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 实验描述 -->
 | 
			
		||||
            <div class="form-control">
 | 
			
		||||
              <label class="label">
 | 
			
		||||
                <span class="label-text font-medium">实验描述 *</span>
 | 
			
		||||
              </label>
 | 
			
		||||
              <textarea
 | 
			
		||||
                v-model="newExam.description"
 | 
			
		||||
                class="textarea textarea-bordered w-full h-32"
 | 
			
		||||
                placeholder="详细描述实验内容、目标和要求..."
 | 
			
		||||
                required
 | 
			
		||||
              ></textarea>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 标签 -->
 | 
			
		||||
            <div class="form-control">
 | 
			
		||||
              <div class="flex flex-wrap gap-2 mb-3 min-h-[2rem]">
 | 
			
		||||
                <span
 | 
			
		||||
                  v-for="(tag, index) in newExam.tags"
 | 
			
		||||
                  :key="index"
 | 
			
		||||
                  class="badge badge-primary gap-2"
 | 
			
		||||
                >
 | 
			
		||||
                  {{ tag }}
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    @click="removeTag(index)"
 | 
			
		||||
                    class="text-primary-content hover:text-error"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-3 h-3"
 | 
			
		||||
                      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>
 | 
			
		||||
                </span>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="flex gap-2">
 | 
			
		||||
                <input
 | 
			
		||||
                  type="text"
 | 
			
		||||
                  v-model="newTagInput"
 | 
			
		||||
                  @keydown.enter.prevent="addTag"
 | 
			
		||||
                  class="input input-bordered flex-1"
 | 
			
		||||
                  placeholder="输入标签按回车添加"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 难度等级 -->
 | 
			
		||||
            <div class="form-control">
 | 
			
		||||
              <div class="flex items-center justify-between p-4 rounded-lg">
 | 
			
		||||
                <span class="label-text font-medium">难度等级 *</span>
 | 
			
		||||
                <div class="flex items-center gap-4">
 | 
			
		||||
                  <div class="rating rating-lg">
 | 
			
		||||
                    <input
 | 
			
		||||
                      v-for="i in 5"
 | 
			
		||||
                      :key="i"
 | 
			
		||||
                      type="radio"
 | 
			
		||||
                      :value="i"
 | 
			
		||||
                      v-model="newExam.difficulty"
 | 
			
		||||
                      class="mask mask-star-2 bg-orange-400"
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <span class="text-lg font-medium text-base-content"
 | 
			
		||||
                    >({{ newExam.difficulty }}/5)</span
 | 
			
		||||
                  >
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 可见性 -->
 | 
			
		||||
            <div class="form-control">
 | 
			
		||||
              <div class="p-4 rounded-lg">
 | 
			
		||||
                <label class="label cursor-pointer justify-start gap-4">
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="checkbox"
 | 
			
		||||
                    v-model="newExam.isVisibleToUsers"
 | 
			
		||||
                    class="checkbox checkbox-primary"
 | 
			
		||||
                  />
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <span class="label-text font-medium">对学生可见</span>
 | 
			
		||||
                    <div class="text-sm text-base-content/70">
 | 
			
		||||
                      开启后学生可以在实验列表中看到此实验
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </label>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 提交按钮 -->
 | 
			
		||||
            <div class="pt-4 border-t border-base-300">
 | 
			
		||||
              <div class="space-y-3">
 | 
			
		||||
                <button
 | 
			
		||||
                  type="submit"
 | 
			
		||||
                  :disabled="isCreating || !canCreateExam"
 | 
			
		||||
                  class="btn btn-primary w-full"
 | 
			
		||||
                >
 | 
			
		||||
                  <span
 | 
			
		||||
                    v-if="isCreating"
 | 
			
		||||
                    class="loading loading-spinner loading-sm mr-2"
 | 
			
		||||
                  ></span>
 | 
			
		||||
                  {{ isCreating ? "创建中..." : "创建实验" }}
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- 右侧:文件上传 -->
 | 
			
		||||
        <div class="flex-1 p-6 overflow-y-auto">
 | 
			
		||||
          <div class="space-y-6">
 | 
			
		||||
            <h3 class="text-xl font-semibold text-base-content mb-4">
 | 
			
		||||
              资源文件
 | 
			
		||||
            </h3>
 | 
			
		||||
 | 
			
		||||
            <!-- 第一行:MD文档 和 图片资源 -->
 | 
			
		||||
            <div class="grid grid-cols-2 gap-4">
 | 
			
		||||
              <!-- MD文档 -->
 | 
			
		||||
              <div class="space-y-2">
 | 
			
		||||
                <label class="text-sm font-medium text-base-content"
 | 
			
		||||
                  >MD文档 (必需)</label
 | 
			
		||||
                >
 | 
			
		||||
                <div
 | 
			
		||||
                  class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors aspect-square flex items-center justify-center"
 | 
			
		||||
                  @click="mdFileInput?.click()"
 | 
			
		||||
                  @dragover.prevent
 | 
			
		||||
                  @dragenter.prevent
 | 
			
		||||
                  @drop.prevent="handleMdFileDrop"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="!uploadFiles.mdFile"
 | 
			
		||||
                    class="flex flex-col items-center gap-3"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-12 h-12 text-base-content/40"
 | 
			
		||||
                      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>
 | 
			
		||||
                    <div class="text-sm text-base-content/70 text-center">
 | 
			
		||||
                      <div class="font-medium mb-1">点击或拖拽上传</div>
 | 
			
		||||
                      <div class="text-xs">支持 .md 文件</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div v-else class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-8 h-8 text-success"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <div class="text-xs font-medium text-success text-center">
 | 
			
		||||
                      {{ uploadFiles.mdFile.name }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="text-xs text-base-content/50">点击重新选择</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  ref="mdFileInput"
 | 
			
		||||
                  @change="handleMdFileChange"
 | 
			
		||||
                  accept=".md"
 | 
			
		||||
                  class="hidden"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <!-- 图片资源 -->
 | 
			
		||||
              <div class="space-y-2">
 | 
			
		||||
                <label class="text-sm font-medium text-base-content"
 | 
			
		||||
                  >图片资源 (可选)</label
 | 
			
		||||
                >
 | 
			
		||||
                <div
 | 
			
		||||
                  class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors aspect-square flex items-center justify-center"
 | 
			
		||||
                  @click="imageFilesInput?.click()"
 | 
			
		||||
                  @dragover.prevent
 | 
			
		||||
                  @dragenter.prevent
 | 
			
		||||
                  @drop.prevent="handleImageFilesDrop"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="uploadFiles.imageFiles.length === 0"
 | 
			
		||||
                    class="flex flex-col items-center gap-3"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-12 h-12 text-base-content/40"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <div class="text-sm text-base-content/70 text-center">
 | 
			
		||||
                      <div class="font-medium mb-1">点击或拖拽上传</div>
 | 
			
		||||
                      <div class="text-xs">支持 PNG, JPG, GIF 等图片格式</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div v-else class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-8 h-8 text-success"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <div class="text-xs font-medium text-success">
 | 
			
		||||
                      {{ uploadFiles.imageFiles.length }} 个文件
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="text-xs text-base-content/50">点击重新选择</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  ref="imageFilesInput"
 | 
			
		||||
                  @change="handleImageFilesChange"
 | 
			
		||||
                  accept="image/*"
 | 
			
		||||
                  multiple
 | 
			
		||||
                  class="hidden"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 第二行:示例比特流 和 画布模板 -->
 | 
			
		||||
            <div class="grid grid-cols-2 gap-4">
 | 
			
		||||
              <!-- 示例比特流 -->
 | 
			
		||||
              <div class="space-y-2">
 | 
			
		||||
                <label class="text-sm font-medium text-base-content"
 | 
			
		||||
                  >示例比特流 (可选)</label
 | 
			
		||||
                >
 | 
			
		||||
                <div
 | 
			
		||||
                  class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors aspect-square flex items-center justify-center"
 | 
			
		||||
                  @click="bitstreamFilesInput?.click()"
 | 
			
		||||
                  @dragover.prevent
 | 
			
		||||
                  @dragenter.prevent
 | 
			
		||||
                  @drop.prevent="handleBitstreamFilesDrop"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="uploadFiles.bitstreamFiles.length === 0"
 | 
			
		||||
                    class="flex flex-col items-center gap-3"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-12 h-12 text-base-content/40"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M13 10V3L4 14h7v7l9-11h-7z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <div class="text-sm text-base-content/70 text-center">
 | 
			
		||||
                      <div class="font-medium mb-1">点击或拖拽上传</div>
 | 
			
		||||
                      <div class="text-xs">支持 .sbit, .bit, .bin 文件</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div v-else class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-8 h-8 text-success"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M13 10V3L4 14h7v7l9-11h-7z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <div class="text-xs font-medium text-success">
 | 
			
		||||
                      {{ uploadFiles.bitstreamFiles.length }} 个文件
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="text-xs text-base-content/50">点击重新选择</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  ref="bitstreamFilesInput"
 | 
			
		||||
                  @change="handleBitstreamFilesChange"
 | 
			
		||||
                  accept=".sbit,.bit,.bin"
 | 
			
		||||
                  multiple
 | 
			
		||||
                  class="hidden"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <!-- 画布模板 -->
 | 
			
		||||
              <div class="space-y-2">
 | 
			
		||||
                <label class="text-sm font-medium text-base-content"
 | 
			
		||||
                  >画布模板 (可选)</label
 | 
			
		||||
                >
 | 
			
		||||
                <div
 | 
			
		||||
                  class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors aspect-square flex items-center justify-center"
 | 
			
		||||
                  @click="canvasFilesInput?.click()"
 | 
			
		||||
                  @dragover.prevent
 | 
			
		||||
                  @dragenter.prevent
 | 
			
		||||
                  @drop.prevent="handleCanvasFilesDrop"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="uploadFiles.canvasFiles.length === 0"
 | 
			
		||||
                    class="flex flex-col items-center gap-3"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-12 h-12 text-base-content/40"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <div class="text-sm text-base-content/70 text-center">
 | 
			
		||||
                      <div class="font-medium mb-1">点击或拖拽上传</div>
 | 
			
		||||
                      <div class="text-xs">支持 .json 文件</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div v-else class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-8 h-8 text-success"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <div class="text-xs font-medium text-success">
 | 
			
		||||
                      {{ uploadFiles.canvasFiles.length }} 个文件
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="text-xs text-base-content/50">点击重新选择</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  ref="canvasFilesInput"
 | 
			
		||||
                  @change="handleCanvasFilesChange"
 | 
			
		||||
                  accept=".json"
 | 
			
		||||
                  multiple
 | 
			
		||||
                  class="hidden"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 第三行:资源包 (单独一个,居中) -->
 | 
			
		||||
            <div class="flex justify-center">
 | 
			
		||||
              <div class="w-1/2 space-y-2">
 | 
			
		||||
                <label class="text-sm font-medium text-base-content"
 | 
			
		||||
                  >资源包 (可选)</label
 | 
			
		||||
                >
 | 
			
		||||
                <div
 | 
			
		||||
                  class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors aspect-square flex items-center justify-center"
 | 
			
		||||
                  @click="resourceFileInput?.click()"
 | 
			
		||||
                  @dragover.prevent
 | 
			
		||||
                  @dragenter.prevent
 | 
			
		||||
                  @drop.prevent="handleResourceFileDrop"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="!uploadFiles.resourceFile"
 | 
			
		||||
                    class="flex flex-col items-center gap-3"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-12 h-12 text-base-content/40"
 | 
			
		||||
                      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>
 | 
			
		||||
                    <div class="text-sm text-base-content/70 text-center">
 | 
			
		||||
                      <div class="font-medium mb-1">点击或拖拽上传</div>
 | 
			
		||||
                      <div class="text-xs">支持 .zip, .rar, .7z 文件</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div v-else class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-8 h-8 text-success"
 | 
			
		||||
                      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>
 | 
			
		||||
                    <div class="text-xs font-medium text-success text-center">
 | 
			
		||||
                      {{ uploadFiles.resourceFile.name }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="text-xs text-base-content/50">点击重新选择</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  ref="resourceFileInput"
 | 
			
		||||
                  @change="handleResourceFileChange"
 | 
			
		||||
                  accept=".zip,.rar,.7z"
 | 
			
		||||
                  class="hidden"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="modal-backdrop" @click="closeCreateModal"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { CreateExamRequest, type FileParameter } from "@/APIClient";
 | 
			
		||||
import { useAlertStore } from "@/components/Alert";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import { useRequiredInjection } from "@/utils/Common";
 | 
			
		||||
import { defineModel, ref, computed } from "vue";
 | 
			
		||||
 | 
			
		||||
const show = defineModel<boolean>("show", {
 | 
			
		||||
  default: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits<{
 | 
			
		||||
  createFinished: [examId: string];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const alertStore = useRequiredInjection(useAlertStore);
 | 
			
		||||
 | 
			
		||||
const newExam = ref({
 | 
			
		||||
  id: "",
 | 
			
		||||
  name: "",
 | 
			
		||||
  description: "",
 | 
			
		||||
  tags: [] as string[],
 | 
			
		||||
  difficulty: 1,
 | 
			
		||||
  isVisibleToUsers: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const isCreating = ref(false);
 | 
			
		||||
const newTagInput = ref("");
 | 
			
		||||
 | 
			
		||||
// 文件上传相关
 | 
			
		||||
const uploadFiles = ref({
 | 
			
		||||
  mdFile: null as File | null,
 | 
			
		||||
  imageFiles: [] as File[],
 | 
			
		||||
  bitstreamFiles: [] as File[],
 | 
			
		||||
  canvasFiles: [] as File[],
 | 
			
		||||
  resourceFile: null as File | null,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 文件输入引用
 | 
			
		||||
const mdFileInput = ref<HTMLInputElement>();
 | 
			
		||||
const imageFilesInput = ref<HTMLInputElement>();
 | 
			
		||||
const bitstreamFilesInput = ref<HTMLInputElement>();
 | 
			
		||||
const canvasFilesInput = ref<HTMLInputElement>();
 | 
			
		||||
const resourceFileInput = ref<HTMLInputElement>();
 | 
			
		||||
 | 
			
		||||
// 计算属性
 | 
			
		||||
const canCreateExam = computed(() => {
 | 
			
		||||
  return (
 | 
			
		||||
    newExam.value.id.trim() !== "" &&
 | 
			
		||||
    newExam.value.name.trim() !== "" &&
 | 
			
		||||
    newExam.value.description.trim() !== "" &&
 | 
			
		||||
    uploadFiles.value.mdFile !== null
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const handleResourceFileChange = (event: Event) => {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  if (target.files && target.files.length > 0) {
 | 
			
		||||
    uploadFiles.value.resourceFile = target.files[0];
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 文件处理方法
 | 
			
		||||
const handleMdFileChange = (event: Event) => {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  if (target.files && target.files.length > 0) {
 | 
			
		||||
    uploadFiles.value.mdFile = target.files[0];
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleMdFileDrop = (event: DragEvent) => {
 | 
			
		||||
  const files = event.dataTransfer?.files;
 | 
			
		||||
  if (files && files.length > 0) {
 | 
			
		||||
    const file = files[0];
 | 
			
		||||
    if (file.name.endsWith(".md")) {
 | 
			
		||||
      uploadFiles.value.mdFile = file;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleImageFilesChange = (event: Event) => {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  if (target.files) {
 | 
			
		||||
    uploadFiles.value.imageFiles = Array.from(target.files);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleImageFilesDrop = (event: DragEvent) => {
 | 
			
		||||
  const files = event.dataTransfer?.files;
 | 
			
		||||
  if (files && files.length > 0) {
 | 
			
		||||
    const imageFiles = Array.from(files).filter((file) =>
 | 
			
		||||
      file.type.startsWith("image/"),
 | 
			
		||||
    );
 | 
			
		||||
    uploadFiles.value.imageFiles = imageFiles;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleBitstreamFilesChange = (event: Event) => {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  if (target.files) {
 | 
			
		||||
    uploadFiles.value.bitstreamFiles = Array.from(target.files);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleBitstreamFilesDrop = (event: DragEvent) => {
 | 
			
		||||
  const files = event.dataTransfer?.files;
 | 
			
		||||
  if (files && files.length > 0) {
 | 
			
		||||
    const bitstreamFiles = Array.from(files).filter(
 | 
			
		||||
      (file) =>
 | 
			
		||||
        file.name.endsWith(".sbit") ||
 | 
			
		||||
        file.name.endsWith(".bit") ||
 | 
			
		||||
        file.name.endsWith(".bin"),
 | 
			
		||||
    );
 | 
			
		||||
    uploadFiles.value.bitstreamFiles = bitstreamFiles;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleCanvasFilesChange = (event: Event) => {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  if (target.files) {
 | 
			
		||||
    uploadFiles.value.canvasFiles = Array.from(target.files);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleCanvasFilesDrop = (event: DragEvent) => {
 | 
			
		||||
  const files = event.dataTransfer?.files;
 | 
			
		||||
  if (files && files.length > 0) {
 | 
			
		||||
    const canvasFiles = Array.from(files).filter((file) =>
 | 
			
		||||
      file.name.endsWith(".json"),
 | 
			
		||||
    );
 | 
			
		||||
    uploadFiles.value.canvasFiles = canvasFiles;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 标签管理
 | 
			
		||||
const addTag = (event?: Event) => {
 | 
			
		||||
  if (event) {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    event.stopPropagation();
 | 
			
		||||
  }
 | 
			
		||||
  const tag = newTagInput.value.trim();
 | 
			
		||||
  if (tag && !newExam.value.tags.includes(tag)) {
 | 
			
		||||
    newExam.value.tags.push(tag);
 | 
			
		||||
    newTagInput.value = "";
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const removeTag = (index: number) => {
 | 
			
		||||
  newExam.value.tags.splice(index, 1);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const resetCreateForm = () => {
 | 
			
		||||
  newExam.value = {
 | 
			
		||||
    id: "",
 | 
			
		||||
    name: "",
 | 
			
		||||
    description: "",
 | 
			
		||||
    tags: [],
 | 
			
		||||
    difficulty: 1,
 | 
			
		||||
    isVisibleToUsers: true,
 | 
			
		||||
  };
 | 
			
		||||
  newTagInput.value = "";
 | 
			
		||||
  uploadFiles.value = {
 | 
			
		||||
    mdFile: null,
 | 
			
		||||
    imageFiles: [],
 | 
			
		||||
    bitstreamFiles: [],
 | 
			
		||||
    canvasFiles: [],
 | 
			
		||||
    resourceFile: null,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // 重置文件输入
 | 
			
		||||
  if (mdFileInput.value) mdFileInput.value.value = "";
 | 
			
		||||
  if (imageFilesInput.value) imageFilesInput.value.value = "";
 | 
			
		||||
  if (bitstreamFilesInput.value) bitstreamFilesInput.value.value = "";
 | 
			
		||||
  if (canvasFilesInput.value) canvasFilesInput.value.value = "";
 | 
			
		||||
  if (resourceFileInput.value) resourceFileInput.value.value = "";
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeCreateModal = () => {
 | 
			
		||||
  show.value = false;
 | 
			
		||||
  resetCreateForm();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleResourceFileDrop = (event: DragEvent) => {
 | 
			
		||||
  const files = event.dataTransfer?.files;
 | 
			
		||||
  if (files && files.length > 0) {
 | 
			
		||||
    const file = files[0];
 | 
			
		||||
    if (
 | 
			
		||||
      file.name.endsWith(".zip") ||
 | 
			
		||||
      file.name.endsWith(".rar") ||
 | 
			
		||||
      file.name.endsWith(".7z")
 | 
			
		||||
    ) {
 | 
			
		||||
      uploadFiles.value.resourceFile = file;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 提交创建实验
 | 
			
		||||
const submitCreateExam = async () => {
 | 
			
		||||
  if (isCreating.value) return;
 | 
			
		||||
 | 
			
		||||
  // 验证必填字段
 | 
			
		||||
  if (!newExam.value.id || !newExam.value.name || !newExam.value.description) {
 | 
			
		||||
    alertStore?.error("请填写所有必填字段");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!uploadFiles.value.mdFile) {
 | 
			
		||||
    alertStore?.error("请上传MD文档");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isCreating.value = true;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedExamClient();
 | 
			
		||||
 | 
			
		||||
    // 创建实验请求
 | 
			
		||||
    const createRequest = new CreateExamRequest({
 | 
			
		||||
      id: newExam.value.id,
 | 
			
		||||
      name: newExam.value.name,
 | 
			
		||||
      description: newExam.value.description,
 | 
			
		||||
      tags: newExam.value.tags,
 | 
			
		||||
      difficulty: newExam.value.difficulty,
 | 
			
		||||
      isVisibleToUsers: newExam.value.isVisibleToUsers,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 创建实验
 | 
			
		||||
    const createdExam = await client.createExam(createRequest);
 | 
			
		||||
    console.log("实验创建成功:", createdExam);
 | 
			
		||||
 | 
			
		||||
    // 上传文件
 | 
			
		||||
    await uploadExamResources(createdExam.id);
 | 
			
		||||
 | 
			
		||||
    alertStore?.success("实验创建成功");
 | 
			
		||||
    closeCreateModal();
 | 
			
		||||
    emits("createFinished", createdExam.id);
 | 
			
		||||
  } catch (err: any) {
 | 
			
		||||
    console.error("创建实验失败:", err);
 | 
			
		||||
    alertStore?.error(err.message || "创建实验失败");
 | 
			
		||||
  } finally {
 | 
			
		||||
    isCreating.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 上传实验资源
 | 
			
		||||
const uploadExamResources = async (examId: string) => {
 | 
			
		||||
  const client = AuthManager.createAuthenticatedResourceClient();
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    // 上传MD文档
 | 
			
		||||
    if (uploadFiles.value.mdFile) {
 | 
			
		||||
      const mdFileParam: FileParameter = {
 | 
			
		||||
        data: uploadFiles.value.mdFile,
 | 
			
		||||
        fileName: uploadFiles.value.mdFile.name,
 | 
			
		||||
      };
 | 
			
		||||
      await client.addResource("doc", "template", examId, mdFileParam);
 | 
			
		||||
      console.log("MD文档上传成功");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 上传图片资源
 | 
			
		||||
    for (const imageFile of uploadFiles.value.imageFiles) {
 | 
			
		||||
      const imageFileParam: FileParameter = {
 | 
			
		||||
        data: imageFile,
 | 
			
		||||
        fileName: imageFile.name,
 | 
			
		||||
      };
 | 
			
		||||
      await client.addResource("image", "template", examId, imageFileParam);
 | 
			
		||||
      console.log("图片上传成功:", imageFile.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 上传比特流文件
 | 
			
		||||
    for (const bitstreamFile of uploadFiles.value.bitstreamFiles) {
 | 
			
		||||
      const bitstreamFileParam: FileParameter = {
 | 
			
		||||
        data: bitstreamFile,
 | 
			
		||||
        fileName: bitstreamFile.name,
 | 
			
		||||
      };
 | 
			
		||||
      await client.addResource(
 | 
			
		||||
        "bitstream",
 | 
			
		||||
        "template",
 | 
			
		||||
        examId,
 | 
			
		||||
        bitstreamFileParam,
 | 
			
		||||
      );
 | 
			
		||||
      console.log("比特流文件上传成功:", bitstreamFile.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 上传画布模板
 | 
			
		||||
    for (const canvasFile of uploadFiles.value.canvasFiles) {
 | 
			
		||||
      const canvasFileParam: FileParameter = {
 | 
			
		||||
        data: canvasFile,
 | 
			
		||||
        fileName: canvasFile.name,
 | 
			
		||||
      };
 | 
			
		||||
      await client.addResource("canvas", "template", examId, canvasFileParam);
 | 
			
		||||
      console.log("画布模板上传成功:", canvasFile.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 上传资源包
 | 
			
		||||
    if (uploadFiles.value.resourceFile) {
 | 
			
		||||
      const resourceFileParam: FileParameter = {
 | 
			
		||||
        data: uploadFiles.value.resourceFile,
 | 
			
		||||
        fileName: uploadFiles.value.resourceFile.name,
 | 
			
		||||
      };
 | 
			
		||||
      await client.addResource(
 | 
			
		||||
        "resource",
 | 
			
		||||
        "template",
 | 
			
		||||
        examId,
 | 
			
		||||
        resourceFileParam,
 | 
			
		||||
      );
 | 
			
		||||
      console.log("资源包上传成功");
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err: any) {
 | 
			
		||||
    console.error("资源上传失败:", err);
 | 
			
		||||
    alertStore?.error("部分资源上传失败: " + (err.message || "未知错误"));
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="postcss" scoped></style>
 | 
			
		||||
							
								
								
									
										330
									
								
								src/views/Exam/ExamInfoModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								src/views/Exam/ExamInfoModal.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,330 @@
 | 
			
		||||
<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 class="text-sm text-base-content/50 text-center py-4">
 | 
			
		||||
                    暂无提交记录
 | 
			
		||||
                  </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 { 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";
 | 
			
		||||
 | 
			
		||||
const show = defineModel<boolean>("show", {
 | 
			
		||||
  default: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  selectedExam: ExamInfo;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const alertStore = useRequiredInjection(useAlertStore);
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
 | 
			
		||||
// 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>
 | 
			
		||||
							
								
								
									
										294
									
								
								src/views/Exam/Index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								src/views/Exam/Index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,294 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="min-h-screen bg-base-100 p-5">
 | 
			
		||||
    <div class="max-w-7xl mx-auto">
 | 
			
		||||
      <div
 | 
			
		||||
        class="flex justify-between items-center mb-8 pb-6 border-b border-base-300"
 | 
			
		||||
      >
 | 
			
		||||
        <h1 class="text-3xl font-bold text-base-content">实验列表</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        v-if="loading"
 | 
			
		||||
        class="flex flex-col items-center justify-center min-h-[300px]"
 | 
			
		||||
      >
 | 
			
		||||
        <div class="loading loading-spinner loading-lg text-primary mb-4"></div>
 | 
			
		||||
        <p class="text-base-content/70">正在加载实验列表...</p>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        v-else-if="error"
 | 
			
		||||
        class="flex flex-col items-center justify-center min-h-[300px]"
 | 
			
		||||
      >
 | 
			
		||||
        <div class="alert alert-error max-w-md">
 | 
			
		||||
          <svg
 | 
			
		||||
            xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
            class="stroke-current shrink-0 h-6 w-6"
 | 
			
		||||
            fill="none"
 | 
			
		||||
            viewBox="0 0 24 24"
 | 
			
		||||
          >
 | 
			
		||||
            <path
 | 
			
		||||
              stroke-linecap="round"
 | 
			
		||||
              stroke-linejoin="round"
 | 
			
		||||
              stroke-width="2"
 | 
			
		||||
              d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
 | 
			
		||||
            />
 | 
			
		||||
          </svg>
 | 
			
		||||
          <div>
 | 
			
		||||
            <h3 class="font-bold">加载失败</h3>
 | 
			
		||||
            <div class="text-xs">{{ error }}</div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <button @click="refreshExams" class="btn btn-primary mt-4">重试</button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div v-else class="space-y-6">
 | 
			
		||||
        <div
 | 
			
		||||
          v-if="exams.length === 0 && !isAdmin"
 | 
			
		||||
          class="flex flex-col items-center justify-center min-h-[300px] text-center"
 | 
			
		||||
        >
 | 
			
		||||
          <h3 class="text-xl font-semibold text-base-content/70 mb-2">
 | 
			
		||||
            暂无实验
 | 
			
		||||
          </h3>
 | 
			
		||||
          <p class="text-base-content/50">
 | 
			
		||||
            当前没有可用的实验,请联系管理员添加实验内容。
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
          v-else
 | 
			
		||||
          class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
 | 
			
		||||
        >
 | 
			
		||||
          <!-- 管理员添加实验卡片 -->
 | 
			
		||||
          <div
 | 
			
		||||
            v-if="isAdmin"
 | 
			
		||||
            class="card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-200 cursor-pointer hover:scale-[1.02]"
 | 
			
		||||
            @click="showCreateModal = true"
 | 
			
		||||
          >
 | 
			
		||||
            <div class="card-body flex items-center justify-center text-center">
 | 
			
		||||
              <div class="text-primary text-6xl mb-4">+</div>
 | 
			
		||||
              <h3 class="text-lg font-semibold text-primary">添加新实验</h3>
 | 
			
		||||
              <p class="text-sm text-primary/70">点击创建新的实验</p>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div
 | 
			
		||||
            v-for="exam in exams"
 | 
			
		||||
            :key="exam.id"
 | 
			
		||||
            class="card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-200 cursor-pointer hover:scale-[1.02] relative overflow-hidden"
 | 
			
		||||
            @click="viewExam(exam.id)"
 | 
			
		||||
          >
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
              <div class="flex justify-between items-start mb-4">
 | 
			
		||||
                <h3 class="card-title text-base-content">{{ exam.name }}</h3>
 | 
			
		||||
                <span
 | 
			
		||||
                  class="card-title text-sm text-blue-600/50 dark:text-sky-400/50"
 | 
			
		||||
                  >{{ exam.id }}</span
 | 
			
		||||
                >
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <!-- 实验标签 -->
 | 
			
		||||
              <div
 | 
			
		||||
                v-if="exam.tags && exam.tags.length > 0"
 | 
			
		||||
                class="flex flex-wrap gap-1 mb-3"
 | 
			
		||||
              >
 | 
			
		||||
                <span
 | 
			
		||||
                  v-for="tag in exam.tags"
 | 
			
		||||
                  :key="tag"
 | 
			
		||||
                  class="badge badge-outline badge-sm"
 | 
			
		||||
                  >{{ tag }}</span
 | 
			
		||||
                >
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div class="space-y-2 text-sm text-base-content/70">
 | 
			
		||||
                <div class="flex items-center gap-2">
 | 
			
		||||
                  <svg
 | 
			
		||||
                    class="w-4 h-4"
 | 
			
		||||
                    fill="none"
 | 
			
		||||
                    stroke="currentColor"
 | 
			
		||||
                    viewBox="0 0 24 24"
 | 
			
		||||
                  >
 | 
			
		||||
                    <path
 | 
			
		||||
                      stroke-linecap="round"
 | 
			
		||||
                      stroke-linejoin="round"
 | 
			
		||||
                      stroke-width="2"
 | 
			
		||||
                      d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
 | 
			
		||||
                    />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                  <span>创建:{{ formatDate(exam.createdTime) }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="flex items-center gap-2">
 | 
			
		||||
                  <svg
 | 
			
		||||
                    class="w-4 h-4"
 | 
			
		||||
                    fill="none"
 | 
			
		||||
                    stroke="currentColor"
 | 
			
		||||
                    viewBox="0 0 24 24"
 | 
			
		||||
                  >
 | 
			
		||||
                    <path
 | 
			
		||||
                      stroke-linecap="round"
 | 
			
		||||
                      stroke-linejoin="round"
 | 
			
		||||
                      stroke-width="2"
 | 
			
		||||
                      d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
 | 
			
		||||
                    />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                  <span>更新:{{ formatDate(exam.updatedTime) }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 难度书角标识 -->
 | 
			
		||||
            <div
 | 
			
		||||
              class="difficulty-corner"
 | 
			
		||||
              :class="{
 | 
			
		||||
                'difficulty-1': exam.difficulty === 1,
 | 
			
		||||
                'difficulty-2': exam.difficulty === 2,
 | 
			
		||||
                'difficulty-3': exam.difficulty === 3,
 | 
			
		||||
                'difficulty-4': exam.difficulty === 4,
 | 
			
		||||
                'difficulty-5': exam.difficulty === 5,
 | 
			
		||||
              }"
 | 
			
		||||
            ></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 实验详情模态框 -->
 | 
			
		||||
    <ExamInfoModal
 | 
			
		||||
      v-if="selectedExam"
 | 
			
		||||
      v-model:show="showInfoModal"
 | 
			
		||||
      :selectedExam="selectedExam"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <!-- 创建实验模态框 -->
 | 
			
		||||
    <ExamEditModal
 | 
			
		||||
      v-model:show="showCreateModal"
 | 
			
		||||
      @create-finished="handleCreateExamFinished"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, onMounted, computed } from "vue";
 | 
			
		||||
import { useRoute } from "vue-router";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import { type ExamSummary, type ExamInfo } from "@/APIClient";
 | 
			
		||||
import { formatDate } from "@/utils/Common";
 | 
			
		||||
import ExamInfoModal from "./ExamInfoModal.vue";
 | 
			
		||||
import ExamEditModal from "./ExamEditModal.vue";
 | 
			
		||||
 | 
			
		||||
// 响应式数据
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const exams = ref<ExamSummary[]>([]);
 | 
			
		||||
const selectedExam = ref<ExamInfo | null>(null);
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
const error = ref<string>("");
 | 
			
		||||
const isAdmin = ref(false);
 | 
			
		||||
 | 
			
		||||
// Modal
 | 
			
		||||
const showCreateModal = ref(false);
 | 
			
		||||
const showInfoModal = ref(false);
 | 
			
		||||
 | 
			
		||||
// 方法
 | 
			
		||||
const checkAdminStatus = async () => {
 | 
			
		||||
  console.log("检查管理员权限...");
 | 
			
		||||
  try {
 | 
			
		||||
    isAdmin.value = await AuthManager.verifyAdminAuth();
 | 
			
		||||
    console.log("管理员权限:", isAdmin.value);
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.warn("无法验证管理员权限:", err);
 | 
			
		||||
    isAdmin.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const refreshExams = async () => {
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
  error.value = "";
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedExamClient();
 | 
			
		||||
    exams.value = await client.getExamList();
 | 
			
		||||
  } catch (err: any) {
 | 
			
		||||
    error.value = err.message || "获取实验列表失败";
 | 
			
		||||
    console.error("获取实验列表失败:", err);
 | 
			
		||||
  } finally {
 | 
			
		||||
    loading.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const viewExam = async (examId: string) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedExamClient();
 | 
			
		||||
    selectedExam.value = await client.getExam(examId);
 | 
			
		||||
    showInfoModal.value = true;
 | 
			
		||||
  } catch (err: any) {
 | 
			
		||||
    error.value = err.message || "获取实验详情失败";
 | 
			
		||||
    console.error("获取实验详情失败:", err);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function handleCreateExamFinished() {
 | 
			
		||||
  await refreshExams();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 生命周期
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  await checkAdminStatus();
 | 
			
		||||
  await refreshExams();
 | 
			
		||||
 | 
			
		||||
  // 处理路由参数,如果有examId则自动打开该实验的详情模态框
 | 
			
		||||
  const examId = route.query.examId as string;
 | 
			
		||||
  if (examId) {
 | 
			
		||||
    await viewExam(examId);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
/* 难度书角样式 */
 | 
			
		||||
.difficulty-corner {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  width: 0;
 | 
			
		||||
  height: 0;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  z-index: 10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.difficulty-corner::before {
 | 
			
		||||
  content: "";
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  width: 0;
 | 
			
		||||
  height: 0;
 | 
			
		||||
  border-style: solid;
 | 
			
		||||
  border-width: 0 48px 48px 0;
 | 
			
		||||
  transition: all 0.3s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 难度颜色渐变:绿色到红色 */
 | 
			
		||||
.difficulty-1::before {
 | 
			
		||||
  border-color: transparent transparent rgba(6, 199, 77, 0.6) transparent; /* 绿色 80% 透明度 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.difficulty-2::before {
 | 
			
		||||
  border-color: transparent transparent rgba(127, 204, 11, 0.6) transparent; /* 黄绿色 80% 透明度 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.difficulty-3::before {
 | 
			
		||||
  border-color: transparent transparent rgba(255, 191, 0, 0.6) transparent; /* 黄色 80% 透明度 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.difficulty-4::before {
 | 
			
		||||
  border-color: transparent transparent rgba(255, 106, 0, 0.6) transparent; /* 橙色 80% 透明度 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.difficulty-5::before {
 | 
			
		||||
  border-color: transparent transparent rgba(245, 35, 35, 0.6) transparent; /* 红色 80% 透明度 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 悬停效果 */
 | 
			
		||||
.card:hover .difficulty-corner::before {
 | 
			
		||||
  filter: brightness(1.1);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user