feat(exam): 新增词条结果详情查看功能

- 新增后端接口获取指定试卷词条判定结果详情
- 新增前端API调用对应接口
- 在上传结果列表页面点击表格行可弹出详情弹窗
- 新建ExamWordsDetailCard组件展示详细信息
- 显示正确词条和错误词条列表及相关统计信息
- 完善后端数据层及服务层支持详情查询功能
This commit is contained in:
lbw
2025-12-14 15:39:41 +08:00
parent c1b3c92244
commit 1ace63cbe0
10 changed files with 222 additions and 4 deletions

View File

@@ -9,4 +9,10 @@ export function getExamWordsResult(page, size) {
page: page,
size: size
})
}
}
export function getExamWordsDetailResult(id) {
return axios.post('/exam/words/detail', {
id: id
})
}

View File

@@ -0,0 +1,121 @@
<template>
<el-dialog v-model="visible" title="词条记录详情" width="820px" :close-on-click-modal="false">
<div class="space-y-4" v-loading="loading">
<el-card shadow="hover">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<div class="text-gray-500 text-sm">记录ID</div>
<div class="text-base font-medium">{{ detail?.id ?? '-' }}</div>
</div>
<div>
<div class="text-gray-500 text-sm">学生ID</div>
<div class="text-base font-medium">{{ detail?.studentId ?? '-' }}</div>
</div>
<div>
<div class="text-gray-500 text-sm">试题ID</div>
<div class="text-base font-medium">{{ detail?.examWordsId ?? '-' }}</div>
</div>
</div>
<div class="mt-4 flex flex-wrap items-center gap-3">
<el-tag type="success">正确词数{{ detail?.correctWordCount ?? 0 }}</el-tag>
<el-tag type="danger">错误词数{{ detail?.wrongWordCount ?? 0 }}</el-tag>
<el-tag :type="detail?.isFinished === 1 ? 'success' : 'info'">
{{ detail?.isFinished === 1 ? '已完成' : '未完成' }}
</el-tag>
<el-tag>
开始时间{{ detail?.startDate ?? '-' }}
</el-tag>
</div>
<div class="mt-2" v-if="detail?.errorMsg">
<el-alert :title="detail.errorMsg" type="warning" show-icon :closable="false" />
</div>
</el-card>
<el-card shadow="never">
<el-collapse v-model="activeNames">
<el-collapse-item :name="'correct'">
<template #title>
正确词条{{ detail?.correctWordCount ?? 0 }}
</template>
<el-scrollbar style="max-height: 240px">
<div class="flex flex-wrap gap-2">
<template v-if="(detail?.correctWordIds?.length ?? 0) > 0">
<el-tag v-for="id in (detail?.correctWordIds || [])" :key="'c-' + id" type="success"
effect="plain">
{{ id }}
</el-tag>
</template>
<div v-else class="text-gray-500">暂无数据</div>
</div>
</el-scrollbar>
</el-collapse-item>
<el-collapse-item :name="'wrong'">
<template #title>
错误词条{{ detail?.wrongWordCount ?? 0 }}
</template>
<el-scrollbar style="max-height: 240px">
<div class="flex flex-wrap gap-2">
<template v-if="(detail?.wrongWordIds?.length ?? 0) > 0">
<el-tag v-for="id in (detail?.wrongWordIds || [])" :key="'w-' + id" type="danger"
effect="plain">
{{ id }}
</el-tag>
</template>
<div v-else class="text-gray-500">暂无数据</div>
</div>
</el-scrollbar>
</el-collapse-item>
</el-collapse>
</el-card>
</div>
</el-dialog>
<div v-if="false"></div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { getExamWordsDetailResult } from '@/api/exam'
const props = defineProps({
modelValue: { type: Boolean, default: false },
id: { type: [Number, String], required: false }
})
const emit = defineEmits(['update:modelValue'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const loading = ref(false)
const detail = ref(null)
const activeNames = ref(['correct', 'wrong'])
async function fetchDetail() {
if (!props.id && props.id !== 0) return
loading.value = true
try {
const res = await getExamWordsDetailResult(Number(props.id))
const d = res?.data?.data ?? null
detail.value = d
} finally {
loading.value = false
}
}
watch(
() => props.modelValue,
(v) => {
if (v) fetchDetail()
}
)
watch(
() => props.id,
(v) => {
if (visible.value && v !== undefined && v !== null) fetchDetail()
}
)
</script>
<style scoped></style>

View File

@@ -24,6 +24,7 @@
border
class="w-full"
v-loading="loading"
@row-click="handleRowClick"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="studentId" label="学生ID" width="100" />
@@ -53,6 +54,7 @@
</div>
</div>
</div>
<ExamWordsDetailCard v-model="showDetail" :id="selectedId" />
</el-main>
</el-container>
@@ -62,6 +64,7 @@
<script setup>
import Header from '@/layouts/components/Header.vue'
import ExamWordsDetailCard from '@/layouts/components/ExamWordsDetailCard.vue'
import { ref, onMounted } from 'vue'
import { uploadExamWordsPng, getExamWordsResult } from '@/api/exam'
@@ -70,6 +73,8 @@ const pageNo = ref(1)
const pageSize = ref(10)
const totalCount = ref(0)
const loading = ref(false)
const showDetail = ref(false)
const selectedId = ref(null)
async function fetchList() {
loading.value = true
@@ -104,6 +109,11 @@ async function doUpload(options) {
fetchList()
}
function handleRowClick(row) {
selectedId.value = row.id
showDetail.value = true
}
onMounted(() => {
fetchList()
})