- 修复 AgentFillDialog.vue 的 optionIds 类型问题 - 修复 AnswerDetailDialog.vue 多选题答案显示问题 - 修复 QuestionnaireFillDialog.vue 多选题 optionIds 提交问题 - 优化代码类型定义,修复 linter 错误 Closes #questionnaire-fixes
321 lines
9.4 KiB
Vue
321 lines
9.4 KiB
Vue
<template>
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="`答题详情 - ${recordInfo.prisonerName || ''}`"
|
||
width="800px"
|
||
:close-on-click-modal="false"
|
||
destroy-on-close
|
||
>
|
||
<div v-loading="loading" class="answer-detail-dialog">
|
||
<!-- 记录基本信息 -->
|
||
<el-descriptions :column="3" border class="mb-20px">
|
||
<el-descriptions-item label="问卷名称">{{ recordInfo.questionnaireName }}</el-descriptions-item>
|
||
<el-descriptions-item label="罪犯编号">{{ recordInfo.prisonerNo }}</el-descriptions-item>
|
||
<el-descriptions-item label="完成时间">{{ formatDateTime(recordInfo.createTime) }}</el-descriptions-item>
|
||
<el-descriptions-item label="客观分">{{ recordInfo.objectiveScore || 0 }}</el-descriptions-item>
|
||
<el-descriptions-item label="主观分">{{ recordInfo.subjectiveScore || 0 }}</el-descriptions-item>
|
||
<el-descriptions-item label="总分">{{ recordInfo.totalScore || 0 }}</el-descriptions-item>
|
||
<el-descriptions-item label="答题用时">{{ formatDuration(recordInfo.duration) }}</el-descriptions-item>
|
||
<el-descriptions-item label="及格状态">
|
||
<el-tag :type="getPassStatusTag(recordInfo.passStatus)" size="small">
|
||
{{ getPassStatusText(recordInfo.passStatus) }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<!-- 问卷题目 -->
|
||
<div v-if="questions.length > 0" class="questionnaire-content">
|
||
<div
|
||
v-for="(item, index) in questions"
|
||
:key="item.id"
|
||
class="question-item"
|
||
>
|
||
<div class="question-header">
|
||
<span class="question-index">{{ index + 1 }}</span>
|
||
<span class="question-title">{{ item.title }}</span>
|
||
<el-tag v-if="item.isRequired" type="danger" size="small" class="required-tag">必填</el-tag>
|
||
<el-tag v-if="item.score" type="info" size="small">{{ item.score }}分</el-tag>
|
||
</div>
|
||
|
||
<!-- 答案展示 -->
|
||
<div class="answer-area">
|
||
<!-- 单选题 -->
|
||
<div v-if="item.type === 1" class="options-container">
|
||
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||
{{ getAnswerText(item.id) }}
|
||
</span>
|
||
<span v-else class="empty-answer">未作答</span>
|
||
</div>
|
||
|
||
<!-- 多选题 -->
|
||
<div v-else-if="item.type === 2" class="options-container">
|
||
<span v-if="getMultiAnswerText(item.id)" class="answer-text">
|
||
{{ getMultiAnswerText(item.id) }}
|
||
</span>
|
||
<span v-else class="empty-answer">未作答</span>
|
||
</div>
|
||
|
||
<!-- 填空题 -->
|
||
<div v-else-if="item.type === 3" class="options-container">
|
||
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||
{{ getAnswerText(item.id) }}
|
||
</span>
|
||
<span v-else class="empty-answer">未作答</span>
|
||
</div>
|
||
|
||
<!-- 评分题 -->
|
||
<div v-else-if="item.type === 4" class="options-container">
|
||
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||
{{ getAnswerText(item.id) }} 分
|
||
</span>
|
||
<span v-else class="empty-answer">未作答</span>
|
||
</div>
|
||
|
||
<!-- 日期题 -->
|
||
<div v-else-if="item.type === 5" class="options-container">
|
||
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||
{{ getAnswerText(item.id) }}
|
||
</span>
|
||
<span v-else class="empty-answer">未作答</span>
|
||
</div>
|
||
|
||
<!-- 数字题 -->
|
||
<div v-else-if="item.type === 6" class="options-container">
|
||
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||
{{ getAnswerText(item.id) }}
|
||
</span>
|
||
<span v-else class="empty-answer">未作答</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-empty v-else description="暂无答题记录" />
|
||
</div>
|
||
|
||
<template #footer>
|
||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { DICT_TYPE } from '@/utils/dict'
|
||
import { formatDateTime } from '@/utils/formatTime'
|
||
import { QuestionnaireRecordApi, type QuestionnaireRecord } from '@/api/prison/questionnairerecord'
|
||
import { QuestionApi, type Question } from '@/api/prison/question'
|
||
import { AnswerApi, type Answer } from '@/api/prison/answer'
|
||
import { getIntDictOptions } from '@/utils/dict'
|
||
|
||
defineOptions({ name: 'AnswerDetailDialog' })
|
||
|
||
const dialogVisible = ref(false)
|
||
const loading = ref(false)
|
||
|
||
// 记录信息
|
||
const recordInfo = ref<QuestionnaireRecord>({
|
||
id: undefined,
|
||
questionnaireName: '',
|
||
prisonerName: '',
|
||
prisonerNo: '',
|
||
objectiveScore: 0,
|
||
subjectiveScore: 0,
|
||
totalScore: 0,
|
||
passStatus: undefined,
|
||
duration: 0,
|
||
createTime: ''
|
||
})
|
||
|
||
// 问题列表
|
||
const questions = ref<Question[]>([])
|
||
|
||
// 答案列表
|
||
const answers = ref<Answer[]>([])
|
||
|
||
/** 格式化时长 */
|
||
const formatDuration = (seconds: number | undefined): string => {
|
||
if (!seconds) return '-'
|
||
const hours = Math.floor(seconds / 3600)
|
||
const minutes = Math.floor((seconds % 3600) / 60)
|
||
const secs = seconds % 60
|
||
if (hours > 0) {
|
||
return `${hours}时${minutes}分${secs}秒`
|
||
} else if (minutes > 0) {
|
||
return `${minutes}分${secs}秒`
|
||
} else {
|
||
return `${secs}秒`
|
||
}
|
||
}
|
||
|
||
/** 获取及格状态文本 */
|
||
const getPassStatusText = (status: number | undefined) => {
|
||
const options = getIntDictOptions(DICT_TYPE.PRISON_RECORD_PASS_STATUS)
|
||
return options.find(o => o.value === status)?.label || '-'
|
||
}
|
||
|
||
/** 获取及格状态标签类型 */
|
||
const getPassStatusTag = (status: number | undefined): 'success' | 'danger' | 'warning' | 'info' => {
|
||
const tagMap: Record<number, 'success' | 'danger' | 'warning' | 'info'> = {
|
||
1: 'success',
|
||
2: 'danger',
|
||
3: 'warning'
|
||
}
|
||
return tagMap[status || 0] || 'info'
|
||
}
|
||
|
||
/** 获取单选/填空/评分/日期/数字题的答案文本 */
|
||
const getAnswerText = (questionId: number | undefined): string | null => {
|
||
if (questionId === undefined) return null
|
||
const answer = answers.value.find(a => a.questionId === questionId)
|
||
return answer?.answerText || null
|
||
}
|
||
|
||
/** 获取多选题的答案文本 */
|
||
const getMultiAnswerText = (questionId: number | undefined): string | null => {
|
||
if (questionId === undefined) return null
|
||
const answer = answers.value.find(a => a.questionId === questionId)
|
||
if (!answer?.optionIds || !Array.isArray(answer.optionIds) || answer.optionIds.length === 0) {
|
||
return null
|
||
}
|
||
|
||
// 找到对应的问题
|
||
const question = questions.value.find(q => q.id === questionId)
|
||
if (!question?.options) return null
|
||
|
||
try {
|
||
// 解析选项
|
||
const options = JSON.parse(question.options) as Array<{ label: string; value?: string; isOther?: boolean }>
|
||
|
||
// 根据 optionIds(索引数组)获取对应的选项文字
|
||
const selectedLabels = answer.optionIds
|
||
.map((idx: number) => options[idx])
|
||
.filter((opt): opt is { label: string } => !!opt)
|
||
.map(opt => opt.label)
|
||
|
||
return selectedLabels.length > 0 ? selectedLabels.join('、') : null
|
||
} catch (e) {
|
||
console.error('解析选项失败:', e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
/** 打开弹窗 */
|
||
const open = async (recordId: number) => {
|
||
dialogVisible.value = true
|
||
loading.value = true
|
||
|
||
try {
|
||
// 并行加载数据
|
||
const [recordData, answerList] = await Promise.all([
|
||
QuestionnaireRecordApi.getQuestionnaireRecord(recordId),
|
||
AnswerApi.getAnswersByAssessmentRecordId(recordId)
|
||
])
|
||
|
||
recordInfo.value = recordData
|
||
answers.value = answerList
|
||
|
||
// 加载问题列表
|
||
if (recordData.questionnaireId) {
|
||
const questionsData = await QuestionApi.getQuestionPage({
|
||
pageNo: 1,
|
||
pageSize: 200,
|
||
questionnaireId: recordData.questionnaireId
|
||
})
|
||
questions.value = questionsData.list
|
||
}
|
||
} catch (error) {
|
||
console.error('加载答题详情失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
defineExpose({ open })
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.answer-detail-dialog {
|
||
overflow-y: auto;
|
||
max-height: 55vh;
|
||
padding-right: 8px;
|
||
|
||
&::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background-color: #e4e7ed;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
.questionnaire-content {
|
||
overflow-y: auto;
|
||
max-height: 50vh;
|
||
padding-right: 8px;
|
||
|
||
&::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background-color: #e4e7ed;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
.question-item {
|
||
margin-bottom: 24px;
|
||
|
||
.question-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
|
||
.question-index {
|
||
flex-shrink: 0;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #409eff;
|
||
color: #fff;
|
||
border-radius: 50%;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.question-title {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
}
|
||
|
||
.answer-area {
|
||
padding-left: 32px;
|
||
|
||
.options-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.answer-text {
|
||
font-size: 14px;
|
||
color: #303133;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.empty-answer {
|
||
color: #c0c4cc;
|
||
font-size: 14px;
|
||
font-style: italic;
|
||
}
|
||
}
|
||
}
|
||
</style>
|