feat(exam): 新增词条结果详情查看功能
- 新增后端接口获取指定试卷词条判定结果详情 - 新增前端API调用对应接口 - 在上传结果列表页面点击表格行可弹出详情弹窗 - 新建ExamWordsDetailCard组件展示详细信息 - 显示正确词条和错误词条列表及相关统计信息 - 完善后端数据层及服务层支持详情查询功能
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
121
enlish-vue/src/layouts/components/ExamWordsDetailCard.vue
Normal file
121
enlish-vue/src/layouts/components/ExamWordsDetailCard.vue
Normal 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>
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user