feat(exam): 新增词条结果详情查看功能
- 新增后端接口获取指定试卷词条判定结果详情 - 新增前端API调用对应接口 - 在上传结果列表页面点击表格行可弹出详情弹窗 - 新建ExamWordsDetailCard组件展示详细信息 - 显示正确词条和错误词条列表及相关统计信息 - 完善后端数据层及服务层支持详情查询功能
This commit is contained in:
@@ -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.ExamWordsJudgeResultDO;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
|
||||||
import com.yinlihupo.enlish.service.model.bo.Word;
|
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.*;
|
||||||
import com.yinlihupo.enlish.service.model.vo.exam.ExamWordsResultRspVO;
|
|
||||||
import com.yinlihupo.enlish.service.model.vo.exam.GenerateExamWordsReqVO;
|
|
||||||
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
|
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
|
||||||
import com.yinlihupo.enlish.service.service.ExamWordsService;
|
import com.yinlihupo.enlish.service.service.ExamWordsService;
|
||||||
import com.yinlihupo.enlish.service.service.VocabularyService;
|
import com.yinlihupo.enlish.service.service.VocabularyService;
|
||||||
@@ -142,4 +140,25 @@ public class ExamWordsController {
|
|||||||
).toList();
|
).toList();
|
||||||
return PageResponse.success(list, page, total, size);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,6 @@ public interface ExamWordsJudgeResultDOMapper {
|
|||||||
List<ExamWordsJudgeResultDO> selectByPage(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
List<ExamWordsJudgeResultDO> selectByPage(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
||||||
|
|
||||||
Integer selectCount();
|
Integer selectCount();
|
||||||
|
|
||||||
|
ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -11,4 +11,6 @@ public interface ExamWordsJudgeService {
|
|||||||
List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize);
|
List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize);
|
||||||
|
|
||||||
Integer getExamWordsJudgeResultCount();
|
Integer getExamWordsJudgeResultCount();
|
||||||
|
|
||||||
|
ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,4 +139,9 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
public Integer getExamWordsJudgeResultCount() {
|
public Integer getExamWordsJudgeResultCount() {
|
||||||
return examWordsJudgeResultDOMapper.selectCount();
|
return examWordsJudgeResultDOMapper.selectCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExamWordsJudgeResultDO getExamWordsJudgeResultDOById(Integer id) {
|
||||||
|
return examWordsJudgeResultDOMapper.selectDetailById(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,5 +61,10 @@
|
|||||||
select count(1)
|
select count(1)
|
||||||
from exam_words_judge_result
|
from exam_words_judge_result
|
||||||
</select>
|
</select>
|
||||||
|
<select id="selectDetailById" resultMap="ResultMapWithBLOBs">
|
||||||
|
select *
|
||||||
|
from exam_words_judge_result
|
||||||
|
where id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -9,4 +9,10 @@ export function getExamWordsResult(page, size) {
|
|||||||
page: page,
|
page: page,
|
||||||
size: size
|
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
|
border
|
||||||
class="w-full"
|
class="w-full"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
|
@row-click="handleRowClick"
|
||||||
>
|
>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column prop="studentId" label="学生ID" width="100" />
|
<el-table-column prop="studentId" label="学生ID" width="100" />
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ExamWordsDetailCard v-model="showDetail" :id="selectedId" />
|
||||||
</el-main>
|
</el-main>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
@@ -62,6 +64,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Header from '@/layouts/components/Header.vue'
|
import Header from '@/layouts/components/Header.vue'
|
||||||
|
import ExamWordsDetailCard from '@/layouts/components/ExamWordsDetailCard.vue'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { uploadExamWordsPng, getExamWordsResult } from '@/api/exam'
|
import { uploadExamWordsPng, getExamWordsResult } from '@/api/exam'
|
||||||
|
|
||||||
@@ -70,6 +73,8 @@ const pageNo = ref(1)
|
|||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const showDetail = ref(false)
|
||||||
|
const selectedId = ref(null)
|
||||||
|
|
||||||
async function fetchList() {
|
async function fetchList() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -104,6 +109,11 @@ async function doUpload(options) {
|
|||||||
fetchList()
|
fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRowClick(row) {
|
||||||
|
selectedId.value = row.id
|
||||||
|
showDetail.value = true
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchList()
|
fetchList()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user