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

@@ -8,9 +8,7 @@ import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.model.bo.Word;
import com.yinlihupo.enlish.service.model.vo.exam.ExamWordsResultReqVO;
import com.yinlihupo.enlish.service.model.vo.exam.ExamWordsResultRspVO;
import com.yinlihupo.enlish.service.model.vo.exam.GenerateExamWordsReqVO;
import com.yinlihupo.enlish.service.model.vo.exam.*;
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
import com.yinlihupo.enlish.service.service.ExamWordsService;
import com.yinlihupo.enlish.service.service.VocabularyService;
@@ -142,4 +140,25 @@ public class ExamWordsController {
).toList();
return PageResponse.success(list, page, total, size);
}
@PostMapping("detail")
@ApiOperationLog(description = "获取试卷结果详情")
Response<ExamWordsDetailResultRspVO> getExamWordsResultDetail(@RequestBody ExamWordsDetailResultReqVO examWordsDetailResultReqVO) {
Integer id = examWordsDetailResultReqVO.getId();
ExamWordsJudgeResultDO examWordsJudgeResultDO = examWordsJudgeService.getExamWordsJudgeResultDOById(id);
ExamWordsDetailResultRspVO examWordsDetailResultRspVO = ExamWordsDetailResultRspVO.builder()
.id(id)
.studentId(examWordsJudgeResultDO.getStudentId())
.examWordsId(examWordsJudgeResultDO.getExamWordsId())
.startDate(examWordsJudgeResultDO.getStartDate())
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
.wrongWordCount(examWordsJudgeResultDO.getWrongWordCount())
.isFinished(examWordsJudgeResultDO.getIsFinished())
.errorMsg(examWordsJudgeResultDO.getErrorMsg())
.correctWordIds(examWordsJudgeResultDO.getCorrectWordIds())
.wrongWordIds(examWordsJudgeResultDO.getWrongWordIds())
.build();
return Response.success(examWordsDetailResultRspVO);
}
}

View File

@@ -18,4 +18,6 @@ public interface ExamWordsJudgeResultDOMapper {
List<ExamWordsJudgeResultDO> selectByPage(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
Integer selectCount();
ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id);
}

View File

@@ -0,0 +1,14 @@
package com.yinlihupo.enlish.service.model.vo.exam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ExamWordsDetailResultReqVO {
private Integer id;
}

View File

@@ -0,0 +1,34 @@
package com.yinlihupo.enlish.service.model.vo.exam;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ExamWordsDetailResultRspVO {
private Integer id;
private String ansSheetPath;
private Integer studentId;
private Integer examWordsId;
private Integer correctWordCount;
private Integer wrongWordCount;
private Integer isFinished;
private LocalDateTime startDate;
private List<Integer> correctWordIds;
private List<Integer> wrongWordIds;
private String errorMsg;
}

View File

@@ -11,4 +11,6 @@ public interface ExamWordsJudgeService {
List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize);
Integer getExamWordsJudgeResultCount();
ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id);
}

View File

@@ -139,4 +139,9 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
public Integer getExamWordsJudgeResultCount() {
return examWordsJudgeResultDOMapper.selectCount();
}
@Override
public ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id) {
return examWordsJudgeResultDOMapper.selectDetailById(id);
}
}

View File

@@ -61,5 +61,10 @@
select count(1)
from exam_words_judge_result
</select>
<select id="selectDetailById" resultMap="ResultMapWithBLOBs">
select *
from exam_words_judge_result
where id = #{id}
</select>
</mapper>

View File

@@ -10,3 +10,9 @@ export function getExamWordsResult(page, size) {
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()
})