tangweijie 535b7be802 fix(questionnaire): 修复问卷模块前端多个问题
- 修复 AgentFillDialog.vue 的 optionIds 类型问题
- 修复 AnswerDetailDialog.vue 多选题答案显示问题
- 修复 QuestionnaireFillDialog.vue 多选题 optionIds 提交问题
- 优化代码类型定义,修复 linter 错误

Closes #questionnaire-fixes
2026-02-04 18:29:46 +08:00

321 lines
9.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>