llm-survey/frontend/src/views/SurveyView.vue

346 lines
9.8 KiB
Vue

<template>
<div class="survey-view">
<van-nav-bar
title="问卷调查"
left-text="返回"
left-arrow
@click-left="onBack"
/>
<div class="survey-content">
<div v-if="surveyStore.currentQuestion" class="question-container">
<h2 class="question-title">
{{ surveyStore.currentQuestionNumber }}. {{ surveyStore.currentQuestion.content }}
<span v-if="surveyStore.currentQuestion.type === 'MULTIPLE_CHOICE'" class="question-type">(多选)</span>
</h2>
<van-checkbox-group v-if="surveyStore.currentQuestion.type === 'MULTIPLE_CHOICE'" v-model="selectedOptions" class="options-container">
<van-cell-group inset>
<van-cell
v-for="option in surveyStore.currentOptions"
:key="option.code"
clickable
@click="toggleOption(option.code)"
>
<template #title>
<span>{{ option.code }}. {{ option.content }}</span>
<template v-if="option.requiresText && selectedOptions.includes(option.code)">
<van-field
v-model="textAnswer"
type="text"
placeholder="请输入具体内容"
class="option-text-input"
@click.stop
/>
</template>
</template>
<template #right-icon>
<van-checkbox
:name="option.code"
@click.stop
/>
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
<van-radio-group v-else-if="surveyStore.currentQuestion.type === 'SINGLE_CHOICE'" v-model="selectedOption" class="options-container">
<van-cell-group inset>
<van-cell
v-for="option in surveyStore.currentOptions"
:key="option.code"
clickable
@click="selectedOption = option.code"
>
<template #title>
<span>{{ option.code }}. {{ option.content }}</span>
<template v-if="option.requiresText && selectedOption === option.code">
<van-field
v-model="textAnswer"
type="text"
placeholder="请输入具体内容"
class="option-text-input"
@click.stop
/>
</template>
</template>
<template #right-icon>
<van-radio :name="option.code" />
</template>
</van-cell>
</van-cell-group>
</van-radio-group>
<div v-else-if="surveyStore.currentQuestion.type === 'TEXT'" class="text-input-container">
<van-field
v-model="textAnswer"
type="textarea"
rows="4"
autosize
placeholder="请输入您的答案"
/>
</div>
<div class="button-container">
<van-button
type="primary"
block
:disabled="!isValidSelection"
@click="onNextQuestion"
>
{{ surveyStore.currentQuestion?.isLast ? '完成' : '下一题' }}
</van-button>
</div>
</div>
<van-empty v-else description="加载中..." />
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { useSurveyStore } from '@/stores/survey';
import { showToast } from 'vant';
const router = useRouter();
const userStore = useUserStore();
const surveyStore = useSurveyStore();
const selectedOption = ref('');
const selectedOptions = ref([]);
const textAnswer = ref('');
const responses = ref([]);
// 计算当前选择是否有效
const isValidSelection = computed(() => {
const questionType = surveyStore.currentQuestion?.type;
if (!questionType) return false;
if (questionType === 'MULTIPLE_CHOICE') {
// 检查是否有选中的选项需要填写文本
const needsText = selectedOptions.value.some(code => {
const option = surveyStore.currentOptions.find(opt => opt.code === code);
return option?.requiresText;
});
return selectedOptions.value.length > 0 && (!needsText || (textAnswer.value?.trim() || '').length > 0);
} else if (questionType === 'SINGLE_CHOICE') {
// 检查单选的选项是否需要文本且已填写
const selectedOpt = surveyStore.currentOptions.find(opt => opt.code === selectedOption.value);
return selectedOption.value && (!selectedOpt?.requiresText || (textAnswer.value?.trim() || '').length > 0);
} else if (questionType === 'TEXT') {
return (textAnswer.value?.trim() || '').length > 0;
}
return false;
});
// 切换多选选项
function toggleOption(code) {
const index = selectedOptions.value.indexOf(code);
if (index === -1) {
selectedOptions.value.push(code);
} else {
selectedOptions.value.splice(index, 1);
}
}
// 返回上一页
function onBack() {
router.back();
}
// 获取下一个问题
async function onNextQuestion() {
const questionType = surveyStore.currentQuestion.type;
try {
let currentSelection = [];
if (questionType === 'MULTIPLE_CHOICE') {
currentSelection = selectedOptions.value;
} else if (questionType === 'SINGLE_CHOICE') {
currentSelection = [selectedOption.value];
}
// 保存当前答案
const currentResponse = {
questionId: surveyStore.currentQuestion.id,
selectedOptions: currentSelection,
textAnswer: textAnswer.value?.trim() || null
};
// 更新或添加当前答案
const existingIndex = responses.value.findIndex(r => r.questionId === currentResponse.questionId);
if (existingIndex !== -1) {
responses.value[existingIndex] = currentResponse;
} else {
responses.value.push(currentResponse);
}
// 保存答题进度到本地存储
localStorage.setItem('surveyProgress', JSON.stringify({
currentQuestionNumber: surveyStore.currentQuestionNumber,
responses: responses.value
}));
// 如果是最后一题,直接提交答案
if (surveyStore.currentQuestion.isLast) {
try {
await surveyStore.submitSurveyResponses(userStore.userId, responses.value);
showToast('问卷提交成功');
surveyStore.isCompleted = true;
// 清除本地存储的进度
localStorage.removeItem('surveyProgress');
// 跳转到完成页面
router.replace('/completed');
return;
} catch (error) {
console.error('提交问卷失败:', error);
showToast(error.response?.data?.message || '提交失败,请重试');
return;
}
}
// 获取下一题
try {
await surveyStore.fetchNextQuestion(userStore.userId, currentSelection);
// 重置选项
selectedOption.value = '';
selectedOptions.value = [];
textAnswer.value = '';
} catch (error) {
console.error('获取下一题失败:', error);
showToast(error.response?.data?.message || '获取下一题失败,请重试');
}
} catch (error) {
console.error('问卷处理失败:', error);
showToast(error.response?.data?.message || '操作失败,请重试');
}
}
// 初始化问卷
async function initSurvey() {
try {
// 检查是否有保存的进度
const savedProgress = localStorage.getItem('surveyProgress');
if (savedProgress) {
const progress = JSON.parse(savedProgress);
responses.value = progress.responses || [];
// 恢复当前题目的选项状态
const lastResponse = responses.value[responses.value.length - 1];
if (lastResponse) {
if (lastResponse.selectedOptions.length === 1) {
selectedOption.value = lastResponse.selectedOptions[0];
} else {
selectedOptions.value = lastResponse.selectedOptions;
}
textAnswer.value = lastResponse.textAnswer || '';
}
// 获取当前问题
const nextQuestionNumber = progress.currentQuestionNumber + 1;
await surveyStore.fetchNextQuestion(
userStore.userId,
[],
nextQuestionNumber
);
// 如果获取失败(可能是最后一题),尝试获取当前题目
if (!surveyStore.currentQuestion) {
await surveyStore.fetchNextQuestion(
userStore.userId,
[],
progress.currentQuestionNumber
);
}
} else {
surveyStore.resetSurvey(); // 确保重置所有状态
responses.value = [];
// 初始化时不传递任何选项
await surveyStore.fetchNextQuestion(userStore.userId);
}
} catch (error) {
console.error('初始化问卷失败:', error);
showToast('初始化问卷失败,请重试');
}
}
// 组件挂载时初始化
onMounted(() => {
if (!userStore.userId) {
router.replace('/register');
return;
}
initSurvey();
});
</script>
<style scoped>
.survey-view {
min-height: 100vh;
background-color: #f7f8fa;
}
.survey-content {
padding: 20px;
}
.question-container {
margin-bottom: 20px;
}
.question-title {
font-size: 18px;
font-weight: bold;
line-height: 1.5;
margin-bottom: 16px;
padding: 0 16px;
}
.options-container {
margin-top: 20px;
}
.button-container {
margin-top: 24px;
padding: 0 16px;
}
.survey-completed {
padding: 40px 20px;
text-align: center;
}
.restart-button {
margin-top: 20px;
width: 80%;
}
.question-type {
font-size: 14px;
color: #666;
margin-left: 8px;
}
.text-input-container {
padding: 16px;
background-color: #fff;
border-radius: 8px;
margin: 0 16px;
}
.completion-message {
margin: 16px 0;
color: #666;
line-height: 1.6;
}
.home-button {
margin-top: 24px;
width: 80%;
}
.option-text-input {
margin-top: 8px;
background-color: #f8f8f8;
border-radius: 4px;
}
</style>