xlcp-frontend/src/views/prison/questionnairerecord/QuestionnaireFillDialog.vue
tangweijie 4bac14276a 重构评估报告模块为评估管理模块
- 重命名report为evaluation-report,更新相关API接口
- 重构评估模板、评估维度、评估报告、快捷评语等模块
- 新增评估管理页面,包含模板配置、维度管理、报告生成等功能
- 更新囚犯工作台,增加评估报告相关功能
- 删除旧的report相关组件
2026-01-19 22:19:23 +08:00

425 lines
11 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>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="900px">
<div v-loading="loading" class="questionnaire-fill-dialog">
<!-- 问卷信息头部 -->
<div class="questionnaire-header">
<h3 class="questionnaire-name">{{ questionnaireName }}</h3>
<p class="questionnaire-tip">请根据实际情况填写以下问卷内容</p>
</div>
<!-- 分区列表 -->
<div class="parts-container">
<el-collapse v-model="activeParts" accordion>
<el-collapse-item
v-for="partition in partitions"
:key="partition.name || 'default'"
:name="partition.name || 'default'"
:title="partition.name || '默认分区'"
>
<template #title>
<div class="part-title">
<span>{{ partition.name || '默认分区' }}</span>
<el-tag size="small" type="info">{{ partition.questions.length }} </el-tag>
</div>
</template>
<!-- 问题列表 -->
<div class="questions-list">
<div
v-for="(question, qIndex) in partition.questions"
:key="question.id"
class="question-item"
:class="{ 'is-required': question.isRequired }"
>
<div class="question-header">
<span class="question-index">{{ qIndex + 1 }}.</span>
<span class="question-title">
{{ question.title }}
<el-tag v-if="question.isRequired" type="danger" size="small">必填</el-tag>
</span>
</div>
<!-- 帮助说明 -->
<div v-if="question.helpText" class="question-help">
<Icon icon="ep:info-filled" />
{{ question.helpText }}
</div>
<!-- 答题区域 -->
<div class="question-answer">
<AutoFill
v-model="answers[question.id!]"
:question-type="question.type"
:fill-type="question.autoFillType || 'NONE'"
:fill-source="question.autoFillSource"
:prisoner-id="prisonerId"
:options="question.options"
:placeholder="question.placeholder"
:is-required="question.isRequired"
:min-value="question.minValue"
:max-value="question.maxValue"
/>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
提交问卷
</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { QuestionApi } from '@/api/prison/question'
import { QuestionnaireRecordApi } from '@/api/prison/questionnairerecord'
import AutoFill from '@/components/AutoFill/index.vue'
import type { Question, AutoFillType } from '@/api/prison/question'
defineOptions({ name: 'QuestionnaireFillDialog' })
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const dialogTitle = ref('问卷填写')
const loading = ref(false)
const submitLoading = ref(false)
// 入参
const recordId = ref<number>(0)
const prisonerId = ref<number>(0)
const questionnaireId = ref<number>(0)
const questionnaireName = ref('')
const recordStatus = ref<number>(0) // 记录状态1-待测评 2-测评中
// 问题列表
const questions = ref<Question[]>([])
// 答案存储:支持 string单选/填空)和 string[](多选)
const answers = ref<Record<number, string | string[] | undefined>>({})
// 展开的分区
const activeParts = ref<string[]>([])
// 分区列表
const partitions = computed(() => {
const partMap = new Map<string, Question[]>()
const defaultPart: Question[] = []
questions.value.forEach(q => {
const partName = q.partName || ''
if (partName) {
if (!partMap.has(partName)) {
partMap.set(partName, [])
}
partMap.get(partName)!.push(q)
} else {
defaultPart.push(q)
}
})
// 构建分区列表
const result: Array<{ name: string; questions: Question[] }> = []
// 添加默认分区
if (defaultPart.length > 0) {
result.push({ name: '', questions: defaultPart })
}
// 添加其他分区(按排序)
Array.from(partMap.entries())
.sort((a, b) => {
const partA = questions.value.find(q => q.partName === a[0])
const partB = questions.value.find(q => q.partName === b[0])
const sortA = partA?.partSort ?? 0
const sortB = partB?.partSort ?? 0
return sortA - sortB
})
.forEach(([name, qs]) => {
result.push({ name, questions: qs })
})
return result
})
// 打开弹窗
const open = async (opts: {
recordId: number
prisonerId: number
questionnaireId: number
questionnaireName: string
}) => {
recordId.value = opts.recordId
prisonerId.value = opts.prisonerId
questionnaireId.value = opts.questionnaireId
questionnaireName.value = opts.questionnaireName
dialogVisible.value = true
// 先获取记录详情,获取当前状态
await loadRecordDetail()
await loadQuestions()
}
// 获取记录详情
const loadRecordDetail = async () => {
if (!recordId.value) return
try {
const res = await QuestionnaireRecordApi.getQuestionnaireRecord(recordId.value)
recordStatus.value = res.status || 0
} catch (e) {
console.error('获取记录详情失败:', e)
}
}
defineExpose({ open })
// 加载问题列表
const loadQuestions = async () => {
if (!questionnaireId.value) return
loading.value = true
try {
const allQuestions: Question[] = []
let pageNo = 1
const pageSize = 200 // 后端每页最大限制
// 循环获取所有问题
while (true) {
const res = await QuestionApi.getQuestionPage({
pageNo,
pageSize,
questionnaireId: questionnaireId.value
})
allQuestions.push(...res.list)
// 如果已经是最后一页,退出循环
if (res.list.length < pageSize || !res.pageInfo || res.pageInfo.total <= allQuestions.length) {
break
}
pageNo++
}
questions.value = allQuestions
// 重置答案
answers.value = {}
questions.value.forEach(q => {
answers.value[q.id!] = undefined
})
// 默认展开第一个分区
if (partitions.value.length > 0) {
activeParts.value = [partitions.value[0].name || 'default']
}
} finally {
loading.value = false
}
}
// 验证必填项
const validateRequired = (): boolean => {
for (const q of questions.value) {
if (q.isRequired) {
const answer = answers.value[q.id!]
// 多选:至少选一个
if (q.type === 2) {
if (!answer || (Array.isArray(answer) && answer.length === 0)) {
return false
}
} else {
// 单选/填空等
if (!answer) {
return false
}
}
}
}
return true
}
// 获取未填写的必填项
const getMissingRequired = (): Question | null => {
for (const q of questions.value) {
if (q.isRequired) {
const answer = answers.value[q.id!]
if (q.type === 2) {
if (!answer || (Array.isArray(answer) && answer.length === 0)) {
return q
}
} else {
if (!answer) {
return q
}
}
}
}
return null
}
// 提交问卷
const handleSubmit = async () => {
// 验证必填项
const missing = getMissingRequired()
if (missing) {
// 找到缺失的分区并展开
const part = partitions.value.find(p => p.questions.some(q => q.id === missing.id))
if (part) {
activeParts.value = [part.name || 'default']
}
// 等待 DOM 更新后滚动到缺失位置
setTimeout(() => {
const el = document.querySelector('.question-item.is-required .el-form-item__error')
el?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 100)
return
}
// 构建答案列表
const answerList = questions.value.map(q => {
const answer = answers.value[q.id!]
// 多选时用逗号分隔
const answerStr = Array.isArray(answer) ? answer.join(',') : answer
return {
questionId: q.id!,
answer: answerStr || '',
optionIds: undefined as number[] | undefined
}
})
submitLoading.value = true
try {
// 如果状态是"待测评(1)",需要先开始测评
if (recordStatus.value === 1) {
await QuestionnaireRecordApi.startAssessment(recordId.value, prisonerId.value)
}
// 提交答卷
await QuestionnaireRecordApi.submitAnswer({
recordId: recordId.value,
prisonerId: prisonerId.value,
answers: answerList
})
// 提交成功后结束测评
await QuestionnaireRecordApi.finishAssessment(recordId.value)
dialogVisible.value = false
emit('success')
} catch (e) {
console.error('提交失败:', e)
} finally {
submitLoading.value = false
}
}
</script>
<style lang="scss" scoped>
.questionnaire-fill-dialog {
max-height: 70vh;
overflow-y: auto;
padding: 0 10px;
}
.questionnaire-header {
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
.questionnaire-name {
margin: 0 0 8px;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.questionnaire-tip {
margin: 0;
color: #909399;
font-size: 14px;
}
}
.parts-container {
:deep(.el-collapse-item__header) {
font-weight: 600;
font-size: 15px;
padding-left: 16px;
}
:deep(.el-collapse-item__content) {
padding-bottom: 0;
}
}
.part-title {
display: flex;
align-items: center;
gap: 12px;
}
.questions-list {
padding: 16px;
}
.question-item {
padding: 16px;
margin-bottom: 12px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #ebeef5;
&:last-child {
margin-bottom: 0;
}
&.is-required {
background: #fff7f7;
border-color: #fbc4c4;
}
}
.question-header {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
}
.question-index {
color: #409eff;
font-weight: 600;
margin-right: 8px;
flex-shrink: 0;
}
.question-title {
font-size: 15px;
color: #303133;
line-height: 1.5;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.question-help {
margin-bottom: 12px;
padding: 8px 12px;
background: #ecf5ff;
border-radius: 4px;
font-size: 13px;
color: #409eff;
display: flex;
align-items: center;
gap: 6px;
}
.question-answer {
margin-top: 8px;
}
</style>