FPGA_WebLab/src/views/Exam/Index.vue

364 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div 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="() => examEditModalRef?.show()"
>
<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="handleCardClicked($event, 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>
<div class="flex flex-row items-center gap-2">
<button
class="btn btn-ghost text-error hover:underline group"
@click="handleEditExamClicked($event, exam.id)"
>
<EditIcon
class="w-4 h-4 transition-transform duration-200 group-hover:scale-110"
/>
编辑
</button>
<span
class="card-title text-sm text-blue-600/50 dark:text-sky-400/50"
>{{ exam.id }}</span
>
</div>
</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
ref="examEditModalRef"
v-model:is-show-modal="showEditModal"
@edit-finished="handleEditExamFinished"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { AuthManager } from "@/utils/AuthManager";
import { ExamClient, type ExamInfo } from "@/APIClient";
import { formatDate } from "@/utils/Common";
import ExamInfoModal from "./ExamInfoModal.vue";
import ExamEditModal from "./ExamEditModal.vue";
import { EditIcon } from "lucide-vue-next";
import { templateRef } from "@vueuse/core";
import { isArray, isNull } from "lodash";
import { watch } from "vue";
import { watchEffect } from "vue";
import { nextTick } from "vue";
const router = useRouter();
const route = useRoute();
// 响应式数据
const exams = ref<ExamInfo[]>([]);
const selectedExam = ref<ExamInfo | null>(null);
const loading = ref(false);
const error = ref<string>("");
const isAdmin = ref(false);
// Modal
const examEditModalRef = templateRef("examEditModalRef");
const showInfoModal = ref(false);
const showEditModal = ref(false);
watch(
() => showInfoModal.value,
() => {
if (isNull(selectedExam.value) || showInfoModal.value == false) {
router.replace({ path: "/exam" });
} else {
router.replace({ path: `/exam/${selectedExam.value.id}` });
}
},
);
watch(
() => showEditModal.value,
() => {
if (showEditModal.value) {
router.replace({
path: `/exam/edit/${examEditModalRef.value?.editExamInfo.id}`,
});
} else {
router.replace({ path: `/exam` });
}
},
);
async function refreshExams() {
loading.value = true;
error.value = "";
try {
const client = AuthManager.createClient(ExamClient);
exams.value = await client.getExamList();
} catch (err: any) {
error.value = err.message || "获取实验列表失败";
console.error("获取实验列表失败:", err);
} finally {
loading.value = false;
}
}
async function viewExam(examId: string) {
try {
const client = AuthManager.createClient(ExamClient);
selectedExam.value = await client.getExam(examId);
showInfoModal.value = true;
} catch (err: any) {
error.value = err.message || "获取实验详情失败";
console.error("获取实验详情失败:", err);
showInfoModal.value = false;
}
}
async function handleEditExamFinished() {
await refreshExams();
}
async function handleCardClicked(event: MouseEvent, examId: string) {
if (event.target instanceof HTMLButtonElement) return;
await viewExam(examId);
}
async function handleEditExamClicked(event: MouseEvent, examId: string) {
await examEditModalRef?.value?.editExam(examId);
router.replace(`/exam/edit/${examId}`);
}
// 生命周期
onMounted(async () => {
const isAuthenticated = await AuthManager.isAuthenticated();
if (!isAuthenticated) {
router.push("/login");
}
isAdmin.value = await AuthManager.isAdminAuthenticated();
await refreshExams();
});
async function loadBasicPage(page: string) {
if (page === "") return;
else if (page === "edit") showEditModal.value = true;
else if (page) await viewExam(page);
else router.push("/exam");
}
onMounted(async () => {
// 处理路由参数如果有examId则自动打开该实验的详情模态框
const page = route.params.page;
if (Array.isArray(page)) {
if (page.length == 1) await loadBasicPage(page[0]);
else if (page.length == 2) {
if (page[0] === "edit") {
await examEditModalRef.value?.editExam(page[1]);
} else {
router.push("/exam");
}
} else router.push("/exam");
} else {
await loadBasicPage(page);
}
});
</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>