feat:考试、练习模块对接完善
This commit is contained in:
parent
e66b21a871
commit
d5bbc38ddf
@ -14,7 +14,12 @@
|
||||
<div class="course-divider"></div>
|
||||
|
||||
<!-- 活动网格 -->
|
||||
<div class="activity-grid">
|
||||
<template v-if="loading">
|
||||
<div class="empty">
|
||||
<n-spin size="medium" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="activity-grid" v-else-if="filteredActivities?.length">
|
||||
<div v-for="activity in filteredActivities" :key="activity.id" class="activity-card">
|
||||
<!-- 活动卡片顶部图片 -->
|
||||
<div class="activity-card-image">
|
||||
@ -70,6 +75,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="empty">
|
||||
<n-empty description="暂无活动"> </n-empty>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -82,6 +92,8 @@ import { ActivityListApi } from '@/api/modules/userCenter'
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 活动接口
|
||||
interface Activity {
|
||||
id: string
|
||||
@ -137,12 +149,15 @@ const viewActivityDetail = (id: string) => {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await ActivityListApi.getActivityList()
|
||||
console.log('活动列表:', response.data.result)
|
||||
activities.value = response.data.result.records || []
|
||||
} catch (error) {
|
||||
message.error('请求活动列表时发生错误')
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -153,6 +168,13 @@ onMounted(async () => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin-top: 10%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 课程筛选标签 */
|
||||
.text-wrapper_1 {
|
||||
width: 100%;
|
||||
|
@ -29,7 +29,7 @@
|
||||
<div class="course-list">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<span>正在加载课程数据...</span>
|
||||
<n-spin size="medium" />
|
||||
</div>
|
||||
<!-- 课程卡片 -->
|
||||
<div
|
||||
@ -132,10 +132,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-state">
|
||||
<span>暂无课程数据</span>
|
||||
<n-empty description="暂无课程数据"> </n-empty>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
472
src/components/profile/ExamContent.vue
Normal file
472
src/components/profile/ExamContent.vue
Normal file
@ -0,0 +1,472 @@
|
||||
<template>
|
||||
<div class="exam-content">
|
||||
<!-- 考试筛选标签 -->
|
||||
<div class="text-wrapper_1 flex-row">
|
||||
<template v-for="tab in tabType" :key="tab.value">
|
||||
<span
|
||||
class="text_12"
|
||||
:class="{ active: activeExamTab === tab.value }"
|
||||
@click="handleExamTabChange(tab.value)"
|
||||
>{{ tab.label }}</span
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="course-divider"></div>
|
||||
|
||||
<!-- 考试列表 -->
|
||||
<template v-if="loading">
|
||||
<div class="empty">
|
||||
<n-spin size="medium" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="exam-grid" v-else-if="examList && examList.length">
|
||||
<div v-for="exam in examList" :key="exam.id" class="exam-card">
|
||||
<!-- 考试标题 -->
|
||||
<div class="exam-title">{{ exam.title }}</div>
|
||||
|
||||
<!-- 分数显示 -->
|
||||
<div class="exam-score-badge" v-if="exam.score !== null">
|
||||
<span class="score-text">{{ exam.score }}<span>分</span></span>
|
||||
</div>
|
||||
|
||||
<!-- 考试信息 -->
|
||||
<div class="exam-details">
|
||||
<div class="exam-meta-item">
|
||||
<span class="meta-label">考试日期:</span>
|
||||
<span class="meta-value">{{ exam.examDate }}</span>
|
||||
</div>
|
||||
<div class="exam-meta-item">
|
||||
<span class="meta-label">考试时间:</span>
|
||||
<span class="meta-value">{{ exam.duration }}分钟</span>
|
||||
</div>
|
||||
<!-- <div class="exam-meta-item">
|
||||
<span class="meta-label">考试题量:</span>
|
||||
<span class="meta-value">{{ exam.questionCount }}题</span>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 考试描述 -->
|
||||
<!-- <div class="exam-description" v-if="exam.description">
|
||||
{{ exam.description }}
|
||||
</div> -->
|
||||
|
||||
<!-- 底部操作区域 -->
|
||||
<div class="exam-footer">
|
||||
<div class="exam-action-right">
|
||||
<NButton
|
||||
v-if="exam.status === '未开始'"
|
||||
disabled
|
||||
@click="startExam(exam.id)"
|
||||
>
|
||||
未开始
|
||||
</NButton>
|
||||
<NButton
|
||||
v-else-if="exam.status === '进行中'"
|
||||
type="primary"
|
||||
@click="continueExam(exam.id)"
|
||||
>
|
||||
开始考试
|
||||
</NButton>
|
||||
<NButton v-else type="primary" @click="viewExamResult(exam.id)">
|
||||
查看详情
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="exam-status-left">
|
||||
<span class="exam-status-text">{{ exam.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="empty">
|
||||
<n-empty description="暂无考试"> </n-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useMessage } from "naive-ui";
|
||||
import { ExamApi } from "@/api/modules/userCenter";
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
const tabType = ref([
|
||||
{ label: "全部考试", value: 4 },
|
||||
{ label: "未开始", value: 1 },
|
||||
{ label: "进行中", value: 2 },
|
||||
{ label: "已结束", value: 3 },
|
||||
]);
|
||||
|
||||
// 定义考试接口
|
||||
interface Exam {
|
||||
id: number;
|
||||
title: string;
|
||||
examDate: string;
|
||||
duration: number;
|
||||
questionCount: number;
|
||||
description: string;
|
||||
status: number | string;
|
||||
score: number | null;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
// 考试筛选状态
|
||||
const activeExamTab = ref(4);
|
||||
|
||||
// 模拟考试数据
|
||||
const examList = ref<Exam[]>([]);
|
||||
|
||||
// 方法
|
||||
const handleExamTabChange = async (tab: number) => {
|
||||
activeExamTab.value = tab;
|
||||
await loadExams();
|
||||
};
|
||||
|
||||
// const getExamStatusText = (status: number) => {
|
||||
// return tabType.value.find((tab) => tab.value === status)?.label || "未知状态";
|
||||
// };
|
||||
|
||||
const startExam = (examId: number) => {
|
||||
message.info(`开始考试 ${examId}`);
|
||||
};
|
||||
|
||||
const continueExam = (examId: number) => {
|
||||
// message.info(`继续考试 ${examId}`)
|
||||
router.push(`/exam/notice/${examId}`)
|
||||
}
|
||||
|
||||
// 查看考试结果
|
||||
const viewExamResult = (examId: number) => {
|
||||
router.push(`/exam-detail/${examId}?source=exam`)
|
||||
}
|
||||
|
||||
const loadExams = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
// 这里可以调用实际的API获取考试数据
|
||||
const response = await ExamApi.getExamList({ type: activeExamTab.value });
|
||||
examList.value =
|
||||
response.data.result?.map((exam: any) => ({
|
||||
id: exam.examId,
|
||||
title: exam.examName,
|
||||
examDate: exam.examStartTime,
|
||||
duration: exam.examTime,
|
||||
questionCount: exam.question_count,
|
||||
description: exam.description,
|
||||
status: exam.examStatus,
|
||||
score: exam.examScore,
|
||||
})) || [];
|
||||
} catch (error) {
|
||||
message.error("加载考试数据失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始加载可以在这里调用API获取考试数据
|
||||
await loadExams();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础样式 */
|
||||
.exam-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 筛选标签样式 */
|
||||
.text-wrapper_1 {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 2.08vh;
|
||||
align-items: center;
|
||||
margin: 20px 0 20px 0;
|
||||
gap: 2.81vw;
|
||||
}
|
||||
|
||||
.text_12 {
|
||||
font-size: 0.94vw;
|
||||
color: #000;
|
||||
font-family: "Microsoft YaHei", Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
padding: 0.42vh 0;
|
||||
transition: color 0.3s ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.text_12.active {
|
||||
color: rgba(2, 134, 206, 1);
|
||||
}
|
||||
|
||||
.text_12:hover {
|
||||
color: rgba(2, 134, 206, 1);
|
||||
}
|
||||
|
||||
/* 分割线样式 */
|
||||
.course-divider {
|
||||
height: 1px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* 考试页面样式 - 网格布局 */
|
||||
.exam-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin-top: 10%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.exam-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.exam-card {
|
||||
background: #ffffff;
|
||||
border: 1.5px solid #d8d8d8;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.exam-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.exam-title {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
border-bottom: 1.5px solid #e6e6e6;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.exam-score-badge {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
right: 18px;
|
||||
background: white;
|
||||
border: 1px solid #ff6f0f;
|
||||
border-radius: 4px;
|
||||
padding: 0 11px;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #ff6f0f;
|
||||
}
|
||||
|
||||
.score-text span {
|
||||
margin-left: 2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.exam-details {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.exam-meta-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #999;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.exam-description {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
color: #497087;
|
||||
margin-bottom: auto;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
background-color: #f5f8fb;
|
||||
}
|
||||
|
||||
.exam-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.exam-status-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.exam-status-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.exam-action-right {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.upcoming-btn {
|
||||
background: #f5f8fb;
|
||||
color: #999999;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upcoming-btn:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.ongoing-btn {
|
||||
background: #0288d1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ongoing-btn:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
.finished-btn {
|
||||
background: #0288d1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.finished-btn:hover {
|
||||
background: #01579b;
|
||||
}
|
||||
|
||||
.files-container {
|
||||
margin: 15px 20px 0 83px;
|
||||
/* width: 100%; */
|
||||
background-color: #f5f9fc;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.file-items span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.files-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.course-name {
|
||||
margin-left: 0px;
|
||||
color: #497087;
|
||||
}
|
||||
|
||||
.course-name span {
|
||||
color: #6aa5cc;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.exam-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.exam-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.exam-card {
|
||||
padding: 16px;
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
/* 考试标题在小屏幕上给分数留出空间并允许换行 */
|
||||
.exam-title {
|
||||
padding-right: 80px;
|
||||
/* 给分数徽章留出空间 */
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* 分数徽章保持在右上角不换行 */
|
||||
.exam-score-badge {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 更小屏幕的适配 */
|
||||
@media (max-width: 480px) {
|
||||
.exam-card {
|
||||
padding: 12px;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.exam-title {
|
||||
padding-right: 70px;
|
||||
/* 给分数徽章留出更多空间 */
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.exam-score-badge {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -91,6 +91,7 @@
|
||||
v-for="item in detailAssignment.aiolHomeworkSubmits"
|
||||
:key="item.id"
|
||||
>
|
||||
<span>已提交:</span>
|
||||
<div class="content" v-html="item.content"></div>
|
||||
<!-- <div class="file">
|
||||
<img src="/images/auth/file.png" alt="" class="files-icon" />
|
||||
@ -224,7 +225,12 @@
|
||||
<div v-if="!showDraftBoxView" class="course-divider"></div>
|
||||
<!-- 作业列表 -->
|
||||
<div>
|
||||
<template v-if="assignments && assignments.length">
|
||||
<template v-if="loading">
|
||||
<div class="empty">
|
||||
<n-spin size="medium" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="assignments && assignments.length">
|
||||
<div
|
||||
v-for="assignment in assignments"
|
||||
:key="assignment.id"
|
||||
@ -347,7 +353,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="empty" v-else>
|
||||
<n-empty description="没有数据"></n-empty>
|
||||
<n-empty description="还没有作业!"></n-empty>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -377,7 +383,7 @@
|
||||
<n-form-item label="上传附件">
|
||||
<n-upload
|
||||
v-model:file-list="uploadForm.files"
|
||||
:max="5"
|
||||
:max="1"
|
||||
multiple
|
||||
:show-file-list="true"
|
||||
:default-upload="false"
|
||||
@ -399,12 +405,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import {
|
||||
NFormItem,
|
||||
NForm,
|
||||
NUpload,
|
||||
useMessage,
|
||||
} from "naive-ui";
|
||||
import { NFormItem, NForm, NUpload, useMessage } from "naive-ui";
|
||||
import { ArrowBack } from "@vicons/ionicons5";
|
||||
// @ts-ignore
|
||||
import QuillEditor from "@/components/common/QuillEditor.vue";
|
||||
@ -417,6 +418,8 @@ const userStore = useUserStore();
|
||||
const formRef = ref();
|
||||
const message = useMessage();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const tabType = ref([
|
||||
{ label: "全部作业", value: 4 },
|
||||
{ label: "未完成", value: 1 },
|
||||
@ -599,7 +602,7 @@ const submitAssignment = async () => {
|
||||
homeworkId: currentAssignment.value?.id,
|
||||
studentId: userStore.user?.id,
|
||||
content: uploadForm.content,
|
||||
status: 2,
|
||||
status: 1,
|
||||
attachment: uploadedFiles
|
||||
.map((file) => {
|
||||
return file.url;
|
||||
@ -623,23 +626,30 @@ const getList = async () => {
|
||||
assignments.value = [];
|
||||
const type =
|
||||
typeof activeHomeworkTab.value === "number" ? activeHomeworkTab.value : 4;
|
||||
const res = await HomeworkApi.getHomeworkList({ type });
|
||||
assignments.value = res.data.result?.map((item: any) => {
|
||||
return {
|
||||
id: item.aiolHomework.id,
|
||||
teacherName: item.realName,
|
||||
teacherAvatar: item.avatar || "",
|
||||
submitCount: item.submitCount || 0,
|
||||
createTime: item.aiolHomework.createTime,
|
||||
endTime: item.aiolHomework.endTime,
|
||||
status: item.aiolHomework.status,
|
||||
title: item.aiolHomework.title,
|
||||
description: item.aiolHomework.description,
|
||||
attachments: item.aiolHomework.attachments || [],
|
||||
mainImage: item.aiolHomework.mainImage || "",
|
||||
aiolHomeworkSubmits: item.aiolHomeworkSubmits || [],
|
||||
};
|
||||
});
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await HomeworkApi.getHomeworkList({ type });
|
||||
assignments.value = res.data.result?.map((item: any) => {
|
||||
return {
|
||||
id: item.aiolHomework.id,
|
||||
teacherName: item.realName,
|
||||
teacherAvatar: item.avatar || "",
|
||||
submitCount: item.submitCount || 0,
|
||||
createTime: item.aiolHomework.createTime,
|
||||
endTime: item.aiolHomework.endTime,
|
||||
status: item.aiolHomework.status,
|
||||
title: item.aiolHomework.title,
|
||||
description: item.aiolHomework.description,
|
||||
attachments: item.aiolHomework.attachments || [],
|
||||
mainImage: item.aiolHomework.mainImage || "",
|
||||
aiolHomeworkSubmits: item.aiolHomeworkSubmits || [],
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
message.error("加载作业数据失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
@ -821,6 +831,9 @@ onMounted(async () => {
|
||||
|
||||
.empty {
|
||||
margin-top: 10%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 作业卡片主容器 */
|
||||
|
@ -2,20 +2,31 @@
|
||||
<div class="practice-content">
|
||||
<!-- 练习筛选标签 -->
|
||||
<div class="text-wrapper_1 flex-row">
|
||||
<span class="text_12" :class="{ active: activePracticeTab === 'all' }"
|
||||
@click="handlePracticeTabChange('all')">全部练习</span>
|
||||
<span class="text_13" :class="{ active: activePracticeTab === 'ongoing' }"
|
||||
@click="handlePracticeTabChange('ongoing')">练习中</span>
|
||||
<span class="text_14" :class="{ active: activePracticeTab === 'finished' }"
|
||||
@click="handlePracticeTabChange('finished')">已结束</span>
|
||||
<template v-for="tab in tabType" :key="tab.value">
|
||||
<span
|
||||
class="text_12"
|
||||
:class="{ active: activePracticeTab === tab.value }"
|
||||
@click="handlePracticeTabChange(tab.value)"
|
||||
>{{ tab.label }}</span
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 练习分割线 -->
|
||||
<div class="course-divider"></div>
|
||||
|
||||
<!-- 练习网格 -->
|
||||
<div class="exam-grid">
|
||||
<div v-for="practice in filteredPractices" :key="practice.id" class="exam-card">
|
||||
<template v-if="loading">
|
||||
<div class="empty">
|
||||
<n-spin size="medium" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="exam-grid" v-else-if="!loading && practiceData?.length">
|
||||
<div
|
||||
v-for="practice in practiceData"
|
||||
:key="practice.id"
|
||||
class="exam-card"
|
||||
>
|
||||
<!-- 考试标题 -->
|
||||
<div class="exam-title">{{ practice.title }}</div>
|
||||
|
||||
@ -34,139 +45,154 @@
|
||||
<span class="meta-label">练习时间:</span>
|
||||
<span class="meta-value">{{ practice.duration }}分钟</span>
|
||||
</div>
|
||||
<div class="exam-meta-item">
|
||||
<!-- <div class="exam-meta-item">
|
||||
<span class="meta-label">题库数量:</span>
|
||||
<span class="meta-value">{{ practice.questionCount }}</span>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 动态显示内容 -->
|
||||
<div v-if="practice.status === 'finished'" class="practice-stats">
|
||||
<div class="stat-row">
|
||||
<!-- <div class="stat-row">
|
||||
<span class="stat-label">答对</span>
|
||||
<span class="stat-value correct">{{ practice.correctCount || 0 }}<span>题</span></span>
|
||||
</div>
|
||||
<span class="stat-value correct"
|
||||
>{{ practice.correctCount || 0 }}<span>题</span></span
|
||||
>
|
||||
</div> -->
|
||||
<!-- 分割线 -->
|
||||
<div class="divider"></div>
|
||||
<!-- <div class="divider"></div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">答错</span>
|
||||
<span class="stat-value wrong">{{ practice.wrongCount || 0 }}<span>题</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="exam-description">
|
||||
{{ practice.description || '该练习包含' + practice.questionCount + '道题目,涵盖了相关知识点。' }}
|
||||
<span class="stat-value wrong"
|
||||
>{{ practice.wrongCount || 0 }}<span>题</span></span
|
||||
>
|
||||
</div> -->
|
||||
</div>
|
||||
<!-- <div v-else class="exam-description">
|
||||
{{
|
||||
practice.description ||
|
||||
"该练习包含" + practice.questionCount + "道题目,涵盖了相关知识点。"
|
||||
}}
|
||||
</div> -->
|
||||
|
||||
<!-- 底部操作区域 -->
|
||||
<div class="exam-footer">
|
||||
<div class="exam-action-right">
|
||||
<button v-show="practice.status === 'ongoing'" class="action-btn ongoing-btn"
|
||||
@click="continuePractice(practice.id)">
|
||||
<NButton
|
||||
v-show="practice.status === '进行中'"
|
||||
type="primary"
|
||||
@click="continuePractice(practice.id)"
|
||||
>
|
||||
继续练习
|
||||
</button>
|
||||
<button v-show="practice.status === 'finished'" class="action-btn finished-btn"
|
||||
@click="viewPracticeDetail(practice.id)">
|
||||
</NButton>
|
||||
<NButton
|
||||
v-show="practice.status === '已结束'"
|
||||
@click="viewPracticeDetail(practice.id)"
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="exam-status-left">
|
||||
<span class="exam-status-text">{{ practice.status === 'ongoing' ? '练习中' : '已结束' }}</span>
|
||||
<span class="exam-status-text">{{
|
||||
practice.status === "进行中" ? "练习中" : "已结束"
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="empty">
|
||||
<n-empty description="暂无练习"> </n-empty>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useMessage } from "naive-ui";
|
||||
import { useRouter } from "vue-router";
|
||||
import { PracticeApi } from "@/api/modules/userCenter";
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
|
||||
const tabType = ref([
|
||||
{ label: "全部练习", value: 4 },
|
||||
{ label: "练习中", value: 2 },
|
||||
{ label: "已结束", value: 3 },
|
||||
]);
|
||||
|
||||
// 练习接口
|
||||
interface Practice {
|
||||
id: number
|
||||
title: string
|
||||
practiceDate: string
|
||||
duration: number
|
||||
questionCount: number
|
||||
description: string
|
||||
status: 'ongoing' | 'finished'
|
||||
score: number | null
|
||||
correctCount: number
|
||||
wrongCount: number
|
||||
id: number;
|
||||
title: string;
|
||||
practiceDate: string;
|
||||
duration: number;
|
||||
questionCount: number;
|
||||
description: string;
|
||||
status: number | string;
|
||||
score: number | null;
|
||||
correctCount: number;
|
||||
wrongCount: number;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
// 练习筛选状态
|
||||
const activePracticeTab = ref('all')
|
||||
const activePracticeTab = ref(4);
|
||||
|
||||
// 模拟练习数据
|
||||
const mockPractices: Practice[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'C++语言程序设计基础练习题',
|
||||
practiceDate: '2025-07-18 10:00',
|
||||
duration: 120,
|
||||
questionCount: 100,
|
||||
description: '练习涵盖C++基础语法、面向对象编程、数据结构等核心内容,旨在全面评估学生对C++编程语言的掌握程度和实际应用能力。',
|
||||
status: 'ongoing',
|
||||
score: null,
|
||||
correctCount: 18,
|
||||
wrongCount: 15
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'C++语言程序设计基础练习题',
|
||||
practiceDate: '2025-07-18 10:00',
|
||||
duration: 88,
|
||||
questionCount: 100,
|
||||
description: '练习涵盖C++基础语法、面向对象编程、数据结构等核心内容,旨在全面评估学生对C++编程语言的掌握程度和实际应用能力。',
|
||||
status: 'finished',
|
||||
score: 75,
|
||||
correctCount: 18,
|
||||
wrongCount: 12
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Java程序设计高级练习',
|
||||
practiceDate: '2025-07-15 14:30',
|
||||
duration: 90,
|
||||
questionCount: 80,
|
||||
description: '深入Java高级特性,包括并发编程、集合框架、网络编程等内容的综合练习。',
|
||||
status: 'finished',
|
||||
score: 88,
|
||||
correctCount: 70,
|
||||
wrongCount: 10
|
||||
}
|
||||
]
|
||||
|
||||
// 获取筛选后的练习
|
||||
const filteredPractices = computed(() => {
|
||||
if (activePracticeTab.value === 'ongoing') {
|
||||
return mockPractices.filter(practice => practice.status === 'ongoing')
|
||||
} else if (activePracticeTab.value === 'finished') {
|
||||
return mockPractices.filter(practice => practice.status === 'finished')
|
||||
}
|
||||
return mockPractices
|
||||
})
|
||||
const practiceData = ref<Practice[]>([]);
|
||||
|
||||
// 处理练习筛选变化
|
||||
const handlePracticeTabChange = (tab: string) => {
|
||||
activePracticeTab.value = tab
|
||||
}
|
||||
const handlePracticeTabChange = async (tab: number) => {
|
||||
activePracticeTab.value = tab;
|
||||
await getPracticeList();
|
||||
};
|
||||
|
||||
// 继续练习
|
||||
const continuePractice = (practiceId: number) => {
|
||||
message.info(`继续练习 ${practiceId}`)
|
||||
}
|
||||
message.info(`继续练习 ${practiceId}`);
|
||||
};
|
||||
|
||||
// 查看练习详情
|
||||
const viewPracticeDetail = (practiceId: number) => {
|
||||
router.push(`/exam-detail/${practiceId}?source=practice`)
|
||||
}
|
||||
router.push(`/exam-detail/${practiceId}?source=practice`);
|
||||
};
|
||||
|
||||
const getPracticeList = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await PracticeApi.getExamList({
|
||||
type: activePracticeTab.value,
|
||||
});
|
||||
console.log("练习列表:", response.data);
|
||||
// 根据响应数据更新practiceData
|
||||
practiceData.value = response.data.result?.map((res: any) => {
|
||||
return {
|
||||
id: res.examId,
|
||||
title: res.examName,
|
||||
practiceDate: res.examStartTime,
|
||||
duration: res.examTime,
|
||||
questionCount: res.question_count || null,
|
||||
description: res.description || null,
|
||||
status: res.examStatus,
|
||||
score: res.score || null,
|
||||
correctCount: res.correct_count || null,
|
||||
wrongCount: res.wrong_count || null,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
message.error("获取练习列表失败,请稍后重试。");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getPracticeList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -174,6 +200,12 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
.practice-content {
|
||||
width: 100%;
|
||||
}
|
||||
.empty {
|
||||
margin-top: 10%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 课程筛选标签 */
|
||||
.text-wrapper_1 {
|
||||
@ -184,12 +216,10 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
gap: 2.81vw;
|
||||
}
|
||||
|
||||
.text_12,
|
||||
.text_13,
|
||||
.text_14 {
|
||||
.text_12 {
|
||||
font-size: 0.94vw;
|
||||
color: #000;
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
font-family: "Microsoft YaHei", Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
padding: 0.42vh 0;
|
||||
@ -197,15 +227,11 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.text_12.active,
|
||||
.text_13.active,
|
||||
.text_14.active {
|
||||
.text_12.active {
|
||||
color: rgba(2, 134, 206, 1);
|
||||
}
|
||||
|
||||
.text_12:hover,
|
||||
.text_13:hover,
|
||||
.text_14:hover {
|
||||
.text_12:hover {
|
||||
color: rgba(2, 134, 206, 1);
|
||||
}
|
||||
|
||||
@ -213,7 +239,7 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
.course-divider {
|
||||
width: 100%;
|
||||
height: 1.5px;
|
||||
background: #E6E6E6;
|
||||
background: #e6e6e6;
|
||||
margin-bottom: 1.67vh;
|
||||
}
|
||||
|
||||
@ -226,10 +252,9 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
|
||||
.exam-card {
|
||||
background: #ffffff;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
border: 1.5px solid #d8d8d8;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
min-height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
@ -245,7 +270,7 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
border-bottom: 1.5px solid #E6E6E6;
|
||||
border-bottom: 1.5px solid #e6e6e6;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
@ -254,7 +279,7 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
top: 11px;
|
||||
right: 18px;
|
||||
background: white;
|
||||
border: 1px solid #FF6F0F;
|
||||
border: 1px solid #ff6f0f;
|
||||
border-radius: 4px;
|
||||
padding: 0 11px;
|
||||
}
|
||||
@ -262,7 +287,7 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
.score-text {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #FF6F0F;
|
||||
color: #ff6f0f;
|
||||
}
|
||||
|
||||
.score-text span {
|
||||
@ -300,14 +325,14 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
background-color: #F5F8FB;
|
||||
background-color: #f5f8fb;
|
||||
}
|
||||
|
||||
.practice-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
height: 54px;
|
||||
background: #F5F8FB;
|
||||
background: #f5f8fb;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
@ -315,7 +340,7 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: #EBEBEB;
|
||||
background: #ebebeb;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
@ -343,11 +368,11 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
}
|
||||
|
||||
.stat-value.correct {
|
||||
color: #3F76ED;
|
||||
color: #3f76ed;
|
||||
}
|
||||
|
||||
.stat-value.wrong {
|
||||
color: #FE2E2F;
|
||||
color: #fe2e2f;
|
||||
}
|
||||
|
||||
.exam-footer {
|
||||
@ -385,7 +410,7 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
}
|
||||
|
||||
.ongoing-btn {
|
||||
background: #0288D1;
|
||||
background: #0288d1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@ -394,12 +419,12 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
}
|
||||
|
||||
.finished-btn {
|
||||
background: #0288D1;
|
||||
background: #0288d1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.finished-btn:hover {
|
||||
background: #01579B;
|
||||
background: #01579b;
|
||||
}
|
||||
|
||||
/* 通用样式 */
|
||||
@ -495,4 +520,4 @@ const viewPracticeDetail = (practiceId: number) => {
|
||||
padding: 0.31vh 0.63vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user