1814 lines
57 KiB
Vue
1814 lines
57 KiB
Vue
<template>
|
||
<view class="questionnaire-page">
|
||
<!-- 加载中 -->
|
||
<view v-if="loading" class="loading-container">
|
||
<u-loading-icon mode="spinner" size="40" color="#409EFF"></u-loading-icon>
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 提交遮罩层 -->
|
||
<view v-if="submitting" class="submit-mask">
|
||
<view class="submit-mask-content">
|
||
<u-loading-icon mode="spinner" size="40" color="#409EFF"></u-loading-icon>
|
||
<text class="submit-mask-text">提交中,请稍候...</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 问卷填写阶段 -->
|
||
<view v-else-if="currentStep === 'fill'" class="fill-step">
|
||
<view class="questionnaire-header">
|
||
<text class="questionnaire-title">{{ questionnaireInfo?.title || '问卷' }}</text>
|
||
<text class="questionnaire-desc" v-if="questionnaireInfo?.description">{{ questionnaireInfo.description }}</text>
|
||
</view>
|
||
|
||
<!-- 分页标题 -->
|
||
<view v-if="pageList.length > 0" class="page-header">
|
||
<text class="page-title">{{ pageList[currentPageIndex]?.pageName || '第一部分' }}</text>
|
||
<text class="page-progress">({{ currentPageIndex + 1 }}/{{ pageList.length }})</text>
|
||
</view>
|
||
|
||
<view class="questions-container">
|
||
<view
|
||
v-for="(question, index) in questionList"
|
||
:key="question.id || index"
|
||
class="question-item"
|
||
:class="{ 'question-item-error': questionErrors[question.id] }"
|
||
>
|
||
<view class="question-header">
|
||
<text class="question-required" v-if="question.required === 1">*</text>
|
||
<text class="question-title">
|
||
第{{ getGlobalQuestionIndex(index) }}题
|
||
<text v-if="question.questionContent" class="question-content-inline">{{ question.questionContent }}</text>
|
||
<text class="question-type-inline">({{ getQuestionTypeName(question.questionType) }})</text>
|
||
</text>
|
||
</view>
|
||
|
||
<!-- 输入框 -->
|
||
<view v-if="question.questionType === 'TEXT'" class="question-input">
|
||
<input
|
||
v-model="answers[question.id].value"
|
||
@input="() => { if (questionErrors[question.id]) questionErrors[question.id] = false; }"
|
||
type="text"
|
||
placeholder="请输入"
|
||
:maxlength="500"
|
||
class="input-field"
|
||
:class="{ 'input-field-error': questionErrors[question.id] }"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 多行文本 -->
|
||
<view v-else-if="question.questionType === 'TEXTAREA'" class="question-input">
|
||
<textarea
|
||
v-model="answers[question.id].value"
|
||
@input="() => { if (questionErrors[question.id]) questionErrors[question.id] = false; }"
|
||
placeholder="请输入"
|
||
:maxlength="2000"
|
||
class="textarea-field"
|
||
:class="{ 'textarea-field-error': questionErrors[question.id] }"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 数字 -->
|
||
<view v-else-if="question.questionType === 'NUMBER'" class="question-input">
|
||
<input
|
||
v-model="answers[question.id].value"
|
||
@input="() => { if (questionErrors[question.id]) questionErrors[question.id] = false; }"
|
||
type="digit"
|
||
placeholder="请输入数字"
|
||
class="input-field"
|
||
:class="{ 'input-field-error': questionErrors[question.id] }"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 单选框 -->
|
||
<view v-else-if="question.questionType === 'RADIO'" class="question-options" :class="{ 'question-options-error': questionErrors[question.id] }">
|
||
<view
|
||
v-for="(option, optIndex) in getQuestionOptions(question)"
|
||
:key="optIndex"
|
||
class="option-item"
|
||
@click="selectRadio(question.id, option.key)"
|
||
>
|
||
<view class="radio-wrapper">
|
||
<view
|
||
class="radio-circle"
|
||
:class="{ 'radio-selected': answers[question.id].value === option.key }"
|
||
>
|
||
<view v-if="answers[question.id].value === option.key" class="radio-inner"></view>
|
||
</view>
|
||
<text class="option-label">{{ option.key }}.</text>
|
||
<text class="option-text">{{ option.value }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 下拉框 -->
|
||
<view v-else-if="question.questionType === 'SELECT'" class="question-input">
|
||
<picker
|
||
:value="getSelectIndex(question.id)"
|
||
:range="getQuestionOptions(question)"
|
||
range-key="value"
|
||
@change="selectPicker($event, question.id)"
|
||
>
|
||
<view class="picker-view" :class="{ 'picker-view-error': questionErrors[question.id] }">
|
||
<text v-if="answers[question.id].value" class="picker-text">
|
||
{{ getOptionText(question, answers[question.id].value) }}
|
||
</text>
|
||
<text v-else class="picker-placeholder">请选择</text>
|
||
<u-icon name="arrow-down" size="16" color="#999"></u-icon>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<!-- 多选框 -->
|
||
<view v-else-if="question.questionType === 'CHECKBOX'" class="question-options" :class="{ 'question-options-error': questionErrors[question.id] }">
|
||
<view
|
||
v-for="(option, optIndex) in getQuestionOptions(question)"
|
||
:key="optIndex"
|
||
class="option-item"
|
||
@click="toggleCheckbox(question.id, option.key)"
|
||
>
|
||
<view class="checkbox-wrapper">
|
||
<view
|
||
class="checkbox-square"
|
||
:class="{ 'checkbox-selected': (answers[question.id].value || []).includes(option.key) }"
|
||
>
|
||
<u-icon
|
||
v-if="(answers[question.id].value || []).includes(option.key)"
|
||
name="checkmark"
|
||
size="12"
|
||
color="#fff"
|
||
></u-icon>
|
||
</view>
|
||
<text class="option-label">{{ option.key }}.</text>
|
||
<text class="option-text">{{ option.value }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日期选择 -->
|
||
<view v-else-if="question.questionType === 'DATE'" class="question-input">
|
||
<picker
|
||
mode="date"
|
||
:value="answers[question.id].value"
|
||
@change="selectDate($event, question.id)"
|
||
>
|
||
<view class="picker-view" :class="{ 'picker-view-error': questionErrors[question.id] }">
|
||
<text v-if="answers[question.id].value" class="picker-text">{{ answers[question.id].value }}</text>
|
||
<text v-else class="picker-placeholder">请选择日期</text>
|
||
<u-icon name="arrow-down" size="16" color="#999"></u-icon>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<!-- 文件上传 -->
|
||
<view v-else-if="question.questionType === 'FILE'" class="question-upload" :class="{ 'question-upload-error': questionErrors[question.id] }">
|
||
<ImageVideoUpload
|
||
v-model:image-list="fileUploadData[question.id].imageList"
|
||
v-model:file-list="fileUploadData[question.id].fileList"
|
||
:max-image-count="10"
|
||
:max-file-count="10"
|
||
:enable-video="false"
|
||
:enable-image="true"
|
||
:enable-file="true"
|
||
:allowed-file-types="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt']"
|
||
:compress-config="compressConfig"
|
||
:upload-api="attachmentUpload"
|
||
@image-upload-success="(image, index) => onImageUploadSuccess(question.id, image, index)"
|
||
@file-upload-success="(file, index) => onFileUploadSuccess(question.id, file, index)"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 视频上传 -->
|
||
<view v-else-if="question.questionType === 'VIDEO'" class="question-upload" :class="{ 'question-upload-error': questionErrors[question.id] }">
|
||
<ImageVideoUpload
|
||
v-model:video-list="fileUploadData[question.id].videoList"
|
||
:max-video-count="5"
|
||
:enable-image="false"
|
||
:enable-file="false"
|
||
:enable-video="true"
|
||
:compress-config="compressConfig"
|
||
:upload-api="attachmentUpload"
|
||
@video-upload-success="(video, index) => onVideoUploadSuccess(question.id, video, index)"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 签名 -->
|
||
<view v-else-if="question.questionType === 'SIGNATURE'" class="question-signature" :class="{ 'question-signature-error': questionErrors[question.id] }">
|
||
<!-- 已有签名:显示预览 -->
|
||
<view v-if="answers[question.id].value" class="signature-preview-inline">
|
||
<image :src="answers[question.id].value" class="signature-preview-img-inline" mode="aspectFit" />
|
||
<view class="signature-actions-inline">
|
||
<u-button
|
||
type="primary"
|
||
size="mini"
|
||
class="resign-btn-inline"
|
||
@click="handleQuestionSignature(question.id)"
|
||
>
|
||
重新签名
|
||
</u-button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 未签名:显示签名按钮 -->
|
||
<view v-else class="signature-placeholder-inline" @click="handleQuestionSignature(question.id)">
|
||
<u-icon name="edit-pen" size="40" color="#999"></u-icon>
|
||
<text class="signature-placeholder-text-inline">点击签名</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 签名区域 -->
|
||
<view v-if="questionnaireInfo && questionnaireInfo.mdqz === 1" class="signature-section">
|
||
<view class="signature-header">
|
||
<text class="signature-label">签名</text>
|
||
<text class="signature-required">*</text>
|
||
</view>
|
||
|
||
<!-- 已有签名:显示预览 -->
|
||
<view v-if="sign_file" class="signature-preview">
|
||
<image :src="sign_file" class="signature-preview-img" mode="aspectFit" />
|
||
<view class="signature-actions">
|
||
<u-button
|
||
type="primary"
|
||
size="mini"
|
||
class="resign-btn"
|
||
@click="handleSignature"
|
||
>
|
||
重新签名
|
||
</u-button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 未签名:显示签名按钮 -->
|
||
<view v-else class="signature-placeholder" @click="handleSignature">
|
||
<u-icon name="edit-pen" size="40" color="#999"></u-icon>
|
||
<text class="signature-placeholder-text">点击签名</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 分页导航按钮 -->
|
||
<view class="page-navigation">
|
||
<button
|
||
v-if="currentPageIndex > 0"
|
||
@click="goToPreviousPage"
|
||
class="nav-btn prev-btn"
|
||
>
|
||
上一页
|
||
</button>
|
||
<button
|
||
v-if="currentPageIndex < pageList.length - 1"
|
||
@click="goToNextPage"
|
||
class="nav-btn next-btn"
|
||
>
|
||
下一页
|
||
</button>
|
||
<button
|
||
v-if="currentPageIndex === pageList.length - 1"
|
||
@click="submitQuestionnaire"
|
||
class="nav-btn submit-btn"
|
||
>
|
||
提交问卷
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 签名组件 -->
|
||
<BasicSign v-if="showSignature" ref="signCompRef" :title="signTitle"></BasicSign>
|
||
|
||
<!-- 提交成功阶段 -->
|
||
<view v-else-if="currentStep === 'success'" class="success-step">
|
||
<view class="success-content">
|
||
<u-icon name="checkmark-circle" size="80" color="#67C23A"></u-icon>
|
||
<text class="success-title">提交成功</text>
|
||
<text class="success-time">{{ formatTime(new Date()) }}</text>
|
||
<text class="success-tip">您已成功提交问卷</text>
|
||
|
||
<!-- 查看AI分析报告按钮 -->
|
||
<button v-if="showAIAnalysisButton" @click="viewAIAnalysisReport" class="ai-analyze-btn">
|
||
<u-icon name="file-text" size="18" color="#fff" style="margin-right: 8rpx;"></u-icon>
|
||
查看AI分析报告
|
||
</button>
|
||
|
||
<!-- 重新填写按钮(当允许重复提交时显示) -->
|
||
<button v-if="allowResubmit" @click="handleResubmit" class="resubmit-btn">
|
||
<u-icon name="reload" size="18" color="#fff" style="margin-right: 8rpx;"></u-icon>
|
||
重新填写
|
||
</button>
|
||
|
||
<button @click="goBack" class="back-btn">返回</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 无权限阶段 -->
|
||
<view v-else-if="currentStep === 'noPermission'" class="no-permission-step">
|
||
<view class="no-permission-content">
|
||
<u-icon name="close-circle" size="80" color="#F56C6C"></u-icon>
|
||
<text class="no-permission-title">无填写权限</text>
|
||
<text class="no-permission-subtitle">抱歉,您没有填写此问卷的权限</text>
|
||
<button @click="goBack" class="back-btn">返回</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 已填写阶段 -->
|
||
<view v-else-if="currentStep === 'alreadyFilled'" class="already-filled-step">
|
||
<view class="already-filled-content">
|
||
<u-icon name="info-circle" size="80" color="#409EFF"></u-icon>
|
||
<text class="already-filled-title">已填写</text>
|
||
<text class="already-filled-subtitle">您已经填写过此问卷</text>
|
||
|
||
<!-- 查看AI分析报告按钮 -->
|
||
<button v-if="showAIAnalysisButton" @click="viewAIAnalysisReport" class="ai-analyze-btn">
|
||
<u-icon name="file-text" size="18" color="#fff" style="margin-right: 8rpx;"></u-icon>
|
||
查看AI分析报告
|
||
</button>
|
||
|
||
<!-- 重新填写按钮(当允许重复提交时显示) -->
|
||
<button v-if="allowResubmit" @click="handleResubmit" class="resubmit-btn">
|
||
<u-icon name="reload" size="18" color="#fff" style="margin-right: 8rpx;"></u-icon>
|
||
重新填写
|
||
</button>
|
||
|
||
<button @click="goBack" class="back-btn">返回</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 时间未开始/已结束阶段 -->
|
||
<view v-else-if="currentStep === 'timeInvalid'" class="time-invalid-step">
|
||
<view class="time-invalid-content">
|
||
<u-icon name="clock" size="80" color="#409EFF"></u-icon>
|
||
<text class="time-invalid-title">{{ timeInvalidMessage }}</text>
|
||
<text class="time-invalid-subtitle">{{ timeInvalidSubtitle }}</text>
|
||
<button @click="goBack" class="back-btn">返回</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, reactive, onMounted, nextTick } from 'vue';
|
||
import { onLoad } from '@dcloudio/uni-app';
|
||
import {
|
||
questionnaireFindByIdApi,
|
||
questionnaireCheckPermissionApi,
|
||
questionnaireQuestionFindPageApi,
|
||
questionnaireAnswerSaveApi,
|
||
questionnaireAnswerFindByUserOrFamilyApi,
|
||
questionnaireAnswerFindDetailApi
|
||
} from '@/api/base/wjApi';
|
||
import { useUserStore } from '@/store/modules/user';
|
||
import { attachmentUpload } from '@/api/system/upload';
|
||
import { ImageVideoUpload, type ImageItem, type FileItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload';
|
||
import BasicSign from '@/components/BasicSign/Sign.vue';
|
||
|
||
const userStore = useUserStore();
|
||
|
||
// 页面参数
|
||
const options = ref<any>({});
|
||
const questionnaireId = ref('');
|
||
const openId = ref<string>(""); // 微信 openId
|
||
const xxtsId = ref<string>(""); // 消息推送ID(从URL参数id获取)
|
||
const xsId = ref<string>(""); // 学生ID(推送链接中的 xsId,用于「同一生多家长」去重)
|
||
const loading = ref(true);
|
||
const submitting = ref(false); // 提交中状态
|
||
const currentStep = ref<'fill' | 'success' | 'noPermission' | 'alreadyFilled' | 'timeInvalid'>('fill');
|
||
|
||
// 问卷信息
|
||
const questionnaireInfo = ref<any>(null);
|
||
const questionList = ref<any[]>([]);
|
||
const answers = reactive<Record<string, any>>({});
|
||
const timeInvalidMessage = ref('');
|
||
const timeInvalidSubtitle = ref('');
|
||
|
||
// 分页相关
|
||
const pageList = ref<Array<{pageId: string, pageName: string, pageSortOrder: number, questions: any[]}>>([]);
|
||
const currentPageIndex = ref(0); // 当前分页索引
|
||
|
||
// 题目验证错误状态(用于红色框标识)
|
||
const questionErrors = reactive<Record<string, boolean>>({});
|
||
|
||
// 文件上传相关数据
|
||
const fileUploadData = reactive<Record<string, {
|
||
imageList: ImageItem[];
|
||
fileList: FileItem[];
|
||
videoList: VideoItem[];
|
||
}>>({});
|
||
|
||
// 压缩配置
|
||
const compressConfig = ref(COMPRESS_PRESETS.high);
|
||
|
||
// 签名组件相关
|
||
const signCompRef = ref<any>(null);
|
||
const signTitle = ref<string>("问卷签名");
|
||
const sign_file = ref<string>("");
|
||
const showSignature = ref<boolean>(false);
|
||
const currentSignatureQuestionId = ref<string>(""); // 当前正在签名的题目ID
|
||
|
||
// 提交批次ID,用于查看AI分析报告
|
||
const submitId = ref<string>('');
|
||
|
||
// 控制是否显示AI智能分析按钮
|
||
const showAIAnalysisButton = ref(false);
|
||
// 控制是否允许重复提交
|
||
const allowResubmit = ref(false);
|
||
|
||
onLoad(async (params) => {
|
||
options.value = params;
|
||
openId.value = params?.openId || '';
|
||
xxtsId.value = params?.id || ''; // 从URL参数id获取xxtsId
|
||
xsId.value = (params?.xsId as string) || '';
|
||
|
||
// 如果有 openId,先进行认证
|
||
if (openId.value) {
|
||
try {
|
||
const loginSuccess = await userStore.loginByOpenId(openId.value);
|
||
if (!loginSuccess) {
|
||
uni.showToast({ title: "认证失败,请重新登录", icon: "none" });
|
||
loading.value = false;
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
console.error("openId认证失败:", e);
|
||
uni.showToast({ title: "认证失败", icon: "none" });
|
||
loading.value = false;
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (params?.questionnaireId) {
|
||
questionnaireId.value = params.questionnaireId;
|
||
initializePage();
|
||
} else {
|
||
uni.showToast({ title: '参数错误', icon: 'none' });
|
||
loading.value = false;
|
||
}
|
||
});
|
||
|
||
// 初始化页面
|
||
const initializePage = async () => {
|
||
try {
|
||
loading.value = true;
|
||
|
||
// 1. 获取问卷信息
|
||
const questionnaireResult = await questionnaireFindByIdApi({ id: questionnaireId.value });
|
||
if (questionnaireResult && questionnaireResult.resultCode === 1) {
|
||
questionnaireInfo.value = questionnaireResult.result || questionnaireResult.data;
|
||
console.log('【问卷信息】questionnaireInfo.value:', questionnaireInfo.value);
|
||
console.log('【allowAnalysis值】', questionnaireInfo.value?.allowAnalysis);
|
||
// 判断是否显示AI智能分析按钮:如果 allowAnalysis = 1,则显示(兼容字符串和数字)
|
||
const allowAnalysisValue = questionnaireInfo.value?.allowAnalysis;
|
||
showAIAnalysisButton.value = allowAnalysisValue === 1 || allowAnalysisValue === '1';
|
||
console.log('【是否显示AI按钮】showAIAnalysisButton.value:', showAIAnalysisButton.value);
|
||
|
||
// 判断是否允许重复提交:如果 allowResubmit = 1,则允许(兼容字符串和数字)
|
||
const allowResubmitValue = questionnaireInfo.value?.allowResubmit;
|
||
allowResubmit.value = allowResubmitValue === 1 || allowResubmitValue === '1';
|
||
console.log('【是否允许重复提交】allowResubmit.value:', allowResubmit.value);
|
||
} else {
|
||
throw new Error('获取问卷信息失败');
|
||
}
|
||
|
||
// 2. 检查时间
|
||
const now = new Date();
|
||
if (questionnaireInfo.value.startTime && new Date(questionnaireInfo.value.startTime) > now) {
|
||
currentStep.value = 'timeInvalid';
|
||
timeInvalidMessage.value = '问卷未开始';
|
||
timeInvalidSubtitle.value = `问卷开始时间:${formatTime(questionnaireInfo.value.startTime)}`;
|
||
loading.value = false;
|
||
return;
|
||
}
|
||
if (questionnaireInfo.value.endTime && new Date(questionnaireInfo.value.endTime) < now) {
|
||
currentStep.value = 'timeInvalid';
|
||
timeInvalidMessage.value = '问卷已结束';
|
||
timeInvalidSubtitle.value = `问卷结束时间:${formatTime(questionnaireInfo.value.endTime)}`;
|
||
loading.value = false;
|
||
return;
|
||
}
|
||
|
||
// 3. 检查权限
|
||
const permissionResult = await questionnaireCheckPermissionApi({
|
||
questionnaireId: questionnaireId.value
|
||
});
|
||
if (permissionResult && permissionResult.resultCode === 1) {
|
||
const hasPermission = permissionResult.result || permissionResult.data;
|
||
if (!hasPermission) {
|
||
currentStep.value = 'noPermission';
|
||
loading.value = false;
|
||
return;
|
||
}
|
||
} else {
|
||
throw new Error('检查权限失败');
|
||
}
|
||
|
||
// 4. 检查是否已填写(如果不允许重复提交)
|
||
if (!allowResubmit.value) {
|
||
const answerResult = await questionnaireAnswerFindByUserOrFamilyApi({
|
||
questionnaireId: questionnaireId.value,
|
||
xxtsId: xxtsId.value || undefined,
|
||
xsId: xsId.value || undefined,
|
||
});
|
||
if (answerResult && answerResult.resultCode === 1) {
|
||
const hasAnswered = answerResult.result || answerResult.data;
|
||
if (hasAnswered) {
|
||
currentStep.value = 'alreadyFilled';
|
||
loading.value = false;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. 获取题目列表
|
||
const questionResult = await questionnaireQuestionFindPageApi({
|
||
questionnaireId: questionnaireId.value,
|
||
page: 1,
|
||
rows: 1000
|
||
});
|
||
if (questionResult && questionResult.rows) {
|
||
const allQuestions = questionResult.rows.sort((a: any, b: any) => {
|
||
// 先按分页排序,再按题目排序
|
||
const pageOrderDiff = (a.pageSortOrder || 0) - (b.pageSortOrder || 0);
|
||
if (pageOrderDiff !== 0) return pageOrderDiff;
|
||
return (a.sortOrder || 0) - (b.sortOrder || 0);
|
||
});
|
||
|
||
// 按分页分组
|
||
const pageMap = new Map<string, {pageId: string, pageName: string, pageSortOrder: number, questions: any[]}>();
|
||
|
||
allQuestions.forEach((question: any) => {
|
||
const pageId = question.pageId || 'default';
|
||
const pageName = question.pageName || '第一部分';
|
||
const pageSortOrder = question.pageSortOrder || 0;
|
||
|
||
if (!pageMap.has(pageId)) {
|
||
pageMap.set(pageId, {
|
||
pageId,
|
||
pageName,
|
||
pageSortOrder,
|
||
questions: []
|
||
});
|
||
}
|
||
pageMap.get(pageId)!.questions.push(question);
|
||
});
|
||
|
||
// 转换为数组并按分页排序
|
||
pageList.value = Array.from(pageMap.values()).sort((a, b) =>
|
||
a.pageSortOrder - b.pageSortOrder
|
||
);
|
||
|
||
// 如果没有分页,创建一个默认分页
|
||
if (pageList.value.length === 0) {
|
||
pageList.value = [{
|
||
pageId: 'default',
|
||
pageName: '第一部分',
|
||
pageSortOrder: 0,
|
||
questions: allQuestions
|
||
}];
|
||
}
|
||
|
||
// 设置当前分页为第一个
|
||
currentPageIndex.value = 0;
|
||
|
||
// 更新当前显示的题目列表
|
||
questionList.value = pageList.value[0]?.questions || [];
|
||
|
||
// 调试打印:打印所有题目信息
|
||
console.log('分页列表:', pageList.value);
|
||
console.log('当前分页题目:', questionList.value);
|
||
|
||
// 初始化所有题目的答案
|
||
allQuestions.forEach((question: any) => {
|
||
if (question.questionType === 'CHECKBOX') {
|
||
answers[question.id] = { value: [] };
|
||
} else {
|
||
answers[question.id] = { value: '' };
|
||
}
|
||
|
||
// 初始化文件上传数据
|
||
if (question.questionType === 'FILE') {
|
||
fileUploadData[question.id] = {
|
||
imageList: [],
|
||
fileList: [],
|
||
videoList: []
|
||
};
|
||
} else if (question.questionType === 'VIDEO') {
|
||
fileUploadData[question.id] = {
|
||
imageList: [],
|
||
fileList: [],
|
||
videoList: []
|
||
};
|
||
}
|
||
});
|
||
|
||
// 6. 如果允许重复提交,尝试加载已填写的答案进行回显
|
||
if (allowResubmit.value) {
|
||
await loadExistingAnswers(allQuestions);
|
||
}
|
||
}
|
||
|
||
currentStep.value = 'fill';
|
||
} catch (error: any) {
|
||
console.error('初始化失败:', error);
|
||
uni.showToast({ title: error.message || '加载失败', icon: 'none' });
|
||
currentStep.value = 'noPermission';
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 获取题目的全局序号(跨分页连续编号)
|
||
const getGlobalQuestionIndex = (localIndex: number): number => {
|
||
let globalIndex = 0;
|
||
// 计算前面所有分页的题目总数
|
||
for (let i = 0; i < currentPageIndex.value; i++) {
|
||
globalIndex += pageList.value[i]?.questions.length || 0;
|
||
}
|
||
// 加上当前分页中的题目索引
|
||
globalIndex += localIndex + 1;
|
||
return globalIndex;
|
||
};
|
||
|
||
// 切换到上一页
|
||
const goToPreviousPage = () => {
|
||
if (currentPageIndex.value > 0) {
|
||
currentPageIndex.value--;
|
||
questionList.value = pageList.value[currentPageIndex.value]?.questions || [];
|
||
// 滚动到顶部
|
||
uni.pageScrollTo({
|
||
scrollTop: 0,
|
||
duration: 300
|
||
});
|
||
}
|
||
};
|
||
|
||
// 验证题目是否填写
|
||
const validateQuestion = (question: any): boolean => {
|
||
if (question.required !== 1) {
|
||
return true; // 非必填项,直接通过
|
||
}
|
||
|
||
if (question.questionType === 'FILE') {
|
||
const uploadData = fileUploadData[question.id];
|
||
const hasImage = (uploadData?.imageList || []).some(img => img.url);
|
||
const hasFile = (uploadData?.fileList || []).some(file => file.url);
|
||
return hasImage || hasFile;
|
||
} else if (question.questionType === 'VIDEO') {
|
||
const uploadData = fileUploadData[question.id];
|
||
const hasVideo = (uploadData?.videoList || []).some(video => video.url);
|
||
return hasVideo;
|
||
} else if (question.questionType === 'SIGNATURE') {
|
||
const answer = answers[question.id];
|
||
return answer && answer.value;
|
||
} else {
|
||
const answer = answers[question.id];
|
||
if (!answer || !answer.value) {
|
||
return false;
|
||
}
|
||
if (Array.isArray(answer.value) && answer.value.length === 0) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
};
|
||
|
||
// 切换到下一页
|
||
const goToNextPage = () => {
|
||
// 清除当前分页的错误状态
|
||
const currentPage = pageList.value[currentPageIndex.value];
|
||
if (currentPage) {
|
||
currentPage.questions.forEach(q => {
|
||
questionErrors[q.id] = false;
|
||
});
|
||
}
|
||
|
||
// 验证当前分页的必填项
|
||
if (currentPage) {
|
||
let hasError = false;
|
||
for (const question of currentPage.questions) {
|
||
if (!validateQuestion(question)) {
|
||
questionErrors[question.id] = true;
|
||
hasError = true;
|
||
}
|
||
}
|
||
|
||
if (hasError) {
|
||
uni.showToast({
|
||
title: '请完成所有必填项',
|
||
icon: 'none'
|
||
});
|
||
// 滚动到第一个错误项
|
||
nextTick(() => {
|
||
const query = uni.createSelectorQuery();
|
||
query.select('.question-item-error').boundingClientRect();
|
||
query.exec((res) => {
|
||
if (res && res[0]) {
|
||
uni.pageScrollTo({
|
||
scrollTop: res[0].top - 100,
|
||
duration: 300
|
||
});
|
||
}
|
||
});
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (currentPageIndex.value < pageList.value.length - 1) {
|
||
currentPageIndex.value++;
|
||
questionList.value = pageList.value[currentPageIndex.value]?.questions || [];
|
||
// 滚动到顶部
|
||
uni.pageScrollTo({
|
||
scrollTop: 0,
|
||
duration: 300
|
||
});
|
||
}
|
||
};
|
||
|
||
// 获取题目选项
|
||
const getQuestionOptions = (question: any) => {
|
||
try {
|
||
if (!question.questionConfig) return [];
|
||
const config = typeof question.questionConfig === 'string'
|
||
? JSON.parse(question.questionConfig)
|
||
: question.questionConfig;
|
||
return config.options || [];
|
||
} catch {
|
||
return [];
|
||
}
|
||
};
|
||
|
||
// 获取问题类型名称
|
||
const getQuestionTypeName = (questionType: string) => {
|
||
const typeMap: Record<string, string> = {
|
||
'TEXT': '输入项',
|
||
'TEXTAREA': '富文本',
|
||
'NUMBER': '数字',
|
||
'RADIO': '单选项',
|
||
'SELECT': '下拉项',
|
||
'CHECKBOX': '多选项',
|
||
'DATE': '日期选择',
|
||
'FILE': '文件上传',
|
||
'VIDEO': '视频上传',
|
||
'SIGNATURE': '签名'
|
||
};
|
||
return typeMap[questionType] || '';
|
||
};
|
||
|
||
// 选择单选框
|
||
const selectRadio = (questionId: string, optionKey: string) => {
|
||
answers[questionId].value = optionKey;
|
||
// 清除错误状态
|
||
if (questionErrors[questionId]) {
|
||
questionErrors[questionId] = false;
|
||
}
|
||
};
|
||
|
||
// 选择下拉框
|
||
const selectPicker = (event: any, questionId: string) => {
|
||
const question = questionList.value.find(q => q.id === questionId);
|
||
if (question) {
|
||
const options = getQuestionOptions(question);
|
||
const selectedOption = options[event.detail.value];
|
||
if (selectedOption) {
|
||
answers[questionId].value = selectedOption.key;
|
||
// 清除错误状态
|
||
if (questionErrors[questionId]) {
|
||
questionErrors[questionId] = false;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// 获取下拉框选中索引
|
||
const getSelectIndex = (questionId: string) => {
|
||
const question = questionList.value.find(q => q.id === questionId);
|
||
if (question) {
|
||
const options = getQuestionOptions(question);
|
||
const currentValue = answers[questionId].value;
|
||
return options.findIndex((opt: any) => opt.key === currentValue);
|
||
}
|
||
return 0;
|
||
};
|
||
|
||
// 获取选项文本
|
||
const getOptionText = (question: any, optionKey: string) => {
|
||
const options = getQuestionOptions(question);
|
||
const option = options.find((opt: any) => opt.key === optionKey);
|
||
return option ? `${option.key}. ${option.value}` : '';
|
||
};
|
||
|
||
// 切换多选框
|
||
const toggleCheckbox = (questionId: string, optionKey: string) => {
|
||
const currentValues = answers[questionId].value || [];
|
||
const index = currentValues.indexOf(optionKey);
|
||
if (index > -1) {
|
||
currentValues.splice(index, 1);
|
||
} else {
|
||
currentValues.push(optionKey);
|
||
}
|
||
answers[questionId].value = currentValues;
|
||
};
|
||
|
||
// 选择日期
|
||
const selectDate = (event: any, questionId: string) => {
|
||
answers[questionId].value = event.detail.value;
|
||
// 清除错误状态
|
||
if (questionErrors[questionId]) {
|
||
questionErrors[questionId] = false;
|
||
}
|
||
};
|
||
|
||
// 图片上传成功回调
|
||
const onImageUploadSuccess = (questionId: string, image: ImageItem, index: number) => {
|
||
console.log('图片上传成功:', image, index);
|
||
// 图片上传成功后,URL 已经保存在 image.url 中
|
||
// 清除错误状态
|
||
if (questionErrors[questionId]) {
|
||
questionErrors[questionId] = false;
|
||
}
|
||
};
|
||
|
||
// 文件上传成功回调
|
||
const onFileUploadSuccess = (questionId: string, file: FileItem, index: number) => {
|
||
console.log('文件上传成功:', file, index);
|
||
// 文件上传成功后,URL 已经保存在 file.url 中
|
||
// 清除错误状态
|
||
if (questionErrors[questionId]) {
|
||
questionErrors[questionId] = false;
|
||
}
|
||
};
|
||
|
||
// 视频上传成功回调
|
||
const onVideoUploadSuccess = (questionId: string, video: VideoItem, index: number) => {
|
||
console.log('视频上传成功:', video, index);
|
||
// 视频上传成功后,URL 已经保存在 video.url 中
|
||
// 清除错误状态
|
||
if (questionErrors[questionId]) {
|
||
questionErrors[questionId] = false;
|
||
}
|
||
};
|
||
|
||
// 处理问卷级别签名
|
||
const handleSignature = async () => {
|
||
try {
|
||
showSignature.value = true;
|
||
signTitle.value = "问卷签名";
|
||
currentSignatureQuestionId.value = ""; // 空字符串表示问卷级别签名
|
||
|
||
// 等待组件渲染完成
|
||
await nextTick();
|
||
const data = await signCompRef.value.getSyncSignature();
|
||
sign_file.value = data.base64;
|
||
showSignature.value = false;
|
||
} catch (error) {
|
||
showSignature.value = false;
|
||
// 如果用户取消签名,不显示错误提示
|
||
if (error && typeof error === 'object' && 'message' in error && error.message !== '用户取消') {
|
||
uni.showToast({ title: '签名失败', icon: 'none' });
|
||
}
|
||
}
|
||
};
|
||
|
||
// 处理题目级别签名
|
||
const handleQuestionSignature = async (questionId: string) => {
|
||
try {
|
||
showSignature.value = true;
|
||
const question = questionList.value.find(q => q.id === questionId);
|
||
signTitle.value = question?.questionContent || "签名";
|
||
currentSignatureQuestionId.value = questionId;
|
||
|
||
// 等待组件渲染完成
|
||
await nextTick();
|
||
const data = await signCompRef.value.getSyncSignature();
|
||
// 将签名保存到对应题目的答案中
|
||
if (answers[questionId]) {
|
||
answers[questionId].value = data.base64;
|
||
// 清除错误状态
|
||
if (questionErrors[questionId]) {
|
||
questionErrors[questionId] = false;
|
||
}
|
||
}
|
||
showSignature.value = false;
|
||
} catch (error) {
|
||
showSignature.value = false;
|
||
// 如果用户取消签名,不显示错误提示
|
||
if (error && typeof error === 'object' && 'message' in error && error.message !== '用户取消') {
|
||
uni.showToast({ title: '签名失败', icon: 'none' });
|
||
}
|
||
}
|
||
};
|
||
|
||
// 提交问卷
|
||
const submitQuestionnaire = async () => {
|
||
// 防止重复提交
|
||
if (submitting.value) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 检查是否需要签名
|
||
if (questionnaireInfo.value && questionnaireInfo.value.mdqz === 1) {
|
||
// 如果需要签名但还没有签名,提示用户先签名
|
||
if (!sign_file.value) {
|
||
uni.showToast({
|
||
title: '请先完成签名',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 显示提交遮罩层
|
||
submitting.value = true;
|
||
|
||
// 如果已有签名或不需要签名,直接提交
|
||
await doSubmitQuestionnaire();
|
||
} catch (error: any) {
|
||
console.error('提交失败:', error);
|
||
uni.showToast({ title: error.message || '提交失败', icon: 'none' });
|
||
} finally {
|
||
// 隐藏提交遮罩层
|
||
submitting.value = false;
|
||
}
|
||
};
|
||
|
||
// 执行提交问卷
|
||
const doSubmitQuestionnaire = async () => {
|
||
try {
|
||
// 验证必填项
|
||
for (const question of questionList.value) {
|
||
if (question.required === 1) {
|
||
if (question.questionType === 'FILE') {
|
||
// 文件类型:检查是否有图片或文档
|
||
const uploadData = fileUploadData[question.id];
|
||
const hasImage = (uploadData?.imageList || []).some(img => img.url);
|
||
const hasFile = (uploadData?.fileList || []).some(file => file.url);
|
||
if (!hasImage && !hasFile) {
|
||
uni.showToast({
|
||
title: `第${questionList.value.indexOf(question) + 1}题为必填项`,
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
} else if (question.questionType === 'VIDEO') {
|
||
// 视频类型:检查是否有视频
|
||
const uploadData = fileUploadData[question.id];
|
||
const hasVideo = (uploadData?.videoList || []).some(video => video.url);
|
||
if (!hasVideo) {
|
||
uni.showToast({
|
||
title: `第${questionList.value.indexOf(question) + 1}题为必填项`,
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
} else if (question.questionType === 'SIGNATURE') {
|
||
// 签名类型:检查是否有签名
|
||
const answer = answers[question.id];
|
||
if (!answer || !answer.value) {
|
||
uni.showToast({
|
||
title: `第${questionList.value.indexOf(question) + 1}题为必填项`,
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
} else {
|
||
const answer = answers[question.id];
|
||
if (!answer || !answer.value ||
|
||
(Array.isArray(answer.value) && answer.value.length === 0)) {
|
||
uni.showToast({
|
||
title: `第${questionList.value.indexOf(question) + 1}题为必填项`,
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查是否有文件正在上传
|
||
for (const question of questionList.value) {
|
||
if (question.questionType === 'FILE' || question.questionType === 'VIDEO') {
|
||
const uploadData = fileUploadData[question.id];
|
||
if (uploadData) {
|
||
const hasUploadingImage = (uploadData.imageList || []).some(img => !img.url);
|
||
const hasUploadingFile = (uploadData.fileList || []).some(file => !file.url);
|
||
const hasUploadingVideo = (uploadData.videoList || []).some(video => !video.url);
|
||
if (hasUploadingImage || hasUploadingFile || hasUploadingVideo) {
|
||
uni.showToast({
|
||
title: '请等待上传完成',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 构建提交数据(包含所有分页的题目)
|
||
const allQuestions: any[] = [];
|
||
pageList.value.forEach(page => {
|
||
allQuestions.push(...page.questions);
|
||
});
|
||
|
||
const answerList = allQuestions.map((question: any) => {
|
||
const answer = answers[question.id];
|
||
let answerValue = answer.value;
|
||
let answerScore: number | undefined = undefined;
|
||
|
||
// 处理不同题型的答案格式和分数计算
|
||
if (question.questionType === 'RADIO') {
|
||
// 单选框:根据选择的选项 key 获取分数
|
||
if (answerValue) {
|
||
const options = getQuestionOptions(question);
|
||
const selectedOption = options.find((opt: any) => opt.key === answerValue);
|
||
if (selectedOption && selectedOption.score !== undefined && selectedOption.score !== null) {
|
||
answerScore = selectedOption.score;
|
||
}
|
||
}
|
||
} else if (question.questionType === 'CHECKBOX') {
|
||
// 多选框:累加所有选择选项的分数
|
||
if (Array.isArray(answerValue) && answerValue.length > 0) {
|
||
const options = getQuestionOptions(question);
|
||
let totalScore = 0;
|
||
answerValue.forEach((key: string) => {
|
||
const selectedOption = options.find((opt: any) => opt.key === key);
|
||
if (selectedOption && selectedOption.score !== undefined && selectedOption.score !== null) {
|
||
totalScore += selectedOption.score;
|
||
}
|
||
});
|
||
answerValue = answerValue.join(',');
|
||
answerScore = totalScore > 0 ? totalScore : undefined;
|
||
} else {
|
||
answerValue = Array.isArray(answerValue) ? answerValue.join(',') : answerValue;
|
||
}
|
||
} else if (question.questionType === 'SIGNATURE') {
|
||
// 签名类型:直接使用 base64 字符串
|
||
answerValue = answerValue || '';
|
||
} else if (question.questionType === 'FILE') {
|
||
// 文件类型:合并图片和文档的URL
|
||
const uploadData = fileUploadData[question.id];
|
||
const imageUrls = (uploadData?.imageList || [])
|
||
.filter(img => img.url)
|
||
.map(img => img.url)
|
||
.filter((url): url is string => !!url);
|
||
const fileUrls = (uploadData?.fileList || [])
|
||
.filter(file => file.url)
|
||
.map(file => file.url)
|
||
.filter((url): url is string => !!url);
|
||
answerValue = [...imageUrls, ...fileUrls].join(',');
|
||
} else if (question.questionType === 'VIDEO') {
|
||
// 视频类型:获取视频URL
|
||
const uploadData = fileUploadData[question.id];
|
||
const videoUrls = (uploadData?.videoList || [])
|
||
.filter(video => video.url)
|
||
.map(video => video.url)
|
||
.filter((url): url is string => !!url);
|
||
answerValue = videoUrls.join(',');
|
||
}
|
||
|
||
const answerData: any = {
|
||
questionnaireId: questionnaireId.value,
|
||
questionId: question.id,
|
||
answerContent: String(answerValue || ''),
|
||
questionType: question.questionType
|
||
};
|
||
|
||
// 如果有分数,添加到提交数据中
|
||
if (answerScore !== undefined && answerScore !== null) {
|
||
answerData.answerScore = answerScore;
|
||
}
|
||
|
||
return answerData;
|
||
});
|
||
|
||
// 构建提交参数
|
||
const submitParams: any = {
|
||
questionnaireId: questionnaireId.value,
|
||
answerList: answerList
|
||
};
|
||
|
||
// 如果有签名,添加到提交参数中
|
||
if (sign_file.value) {
|
||
submitParams.signatureImage = sign_file.value;
|
||
}
|
||
|
||
// 如果有xxtsId,添加到提交参数中(用于更新待办状态)
|
||
if (xxtsId.value) {
|
||
submitParams.xxtsId = xxtsId.value;
|
||
}
|
||
if (xsId.value) {
|
||
submitParams.xsId = xsId.value;
|
||
}
|
||
|
||
// 提交答案
|
||
const result = await questionnaireAnswerSaveApi(submitParams);
|
||
|
||
if (result && result.resultCode === 1) {
|
||
// 保存 submitId(从返回结果中获取)
|
||
const resultData = result.result || result.data || {};
|
||
if (resultData.submitId) {
|
||
submitId.value = resultData.submitId;
|
||
}
|
||
currentStep.value = 'success';
|
||
} else {
|
||
throw new Error(result?.message || '提交失败');
|
||
}
|
||
} catch (error: any) {
|
||
console.error('提交失败:', error);
|
||
uni.showToast({ title: error.message || '提交失败', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
// 返回
|
||
const goBack = () => {
|
||
uni.navigateBack();
|
||
};
|
||
|
||
// 格式化时间
|
||
const formatTime = (time: string | Date) => {
|
||
if (!time) return '';
|
||
const date = typeof time === 'string' ? new Date(time) : time;
|
||
return date.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
};
|
||
|
||
// 查看AI分析报告
|
||
const viewAIAnalysisReport = () => {
|
||
// 获取当前用户ID
|
||
const currentUserId = userStore.userdata?.id || userStore.userdata?.userId || '';
|
||
// 直接跳转到分析页面,由分析页面负责调用AI接口并保存结果
|
||
uni.navigateTo({
|
||
url: `/pages/base/wj/wjEvaluate?questionnaireId=${questionnaireId.value}&userId=${currentUserId}`
|
||
});
|
||
};
|
||
|
||
// 加载已填写的答案(用于回显)
|
||
const loadExistingAnswers = async (allQuestions: any[]) => {
|
||
try {
|
||
// 获取当前用户ID
|
||
const currentUserId = userStore.userdata?.id || userStore.userdata?.userId || '';
|
||
if (!currentUserId) {
|
||
console.log('未找到用户ID,跳过加载已填写答案');
|
||
return;
|
||
}
|
||
|
||
// 获取已填写的答案
|
||
const answerResult = await questionnaireAnswerFindDetailApi({
|
||
questionnaireId: questionnaireId.value,
|
||
userId: currentUserId
|
||
});
|
||
|
||
if (answerResult && answerResult.resultCode === 1) {
|
||
const answerList = answerResult.result || answerResult.data || [];
|
||
if (answerList && answerList.length > 0) {
|
||
console.log('找到已填写的答案,开始回显:', answerList);
|
||
|
||
// 将答案按题目ID建立映射
|
||
const answerMap = new Map<string, any>();
|
||
answerList.forEach((answer: any) => {
|
||
answerMap.set(answer.questionId, answer);
|
||
});
|
||
|
||
// 回显答案到表单
|
||
allQuestions.forEach((question: any) => {
|
||
const existingAnswer = answerMap.get(question.id);
|
||
if (existingAnswer && existingAnswer.answerContent) {
|
||
const answerContent = existingAnswer.answerContent;
|
||
|
||
if (question.questionType === 'CHECKBOX') {
|
||
// 多选框:逗号分隔的字符串转为数组
|
||
if (answerContent) {
|
||
answers[question.id].value = answerContent.split(',').map((v: string) => v.trim()).filter((v: string) => v);
|
||
}
|
||
} else if (question.questionType === 'FILE') {
|
||
// 文件类型:解析URL字符串
|
||
if (answerContent) {
|
||
const urls = answerContent.split(',').map((v: string) => v.trim()).filter((v: string) => v);
|
||
const imageUrls: string[] = [];
|
||
const fileUrls: string[] = [];
|
||
|
||
urls.forEach((url: string) => {
|
||
const lowerUrl = url.toLowerCase();
|
||
// 判断是图片还是文档
|
||
if (lowerUrl.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i)) {
|
||
imageUrls.push(url);
|
||
} else {
|
||
fileUrls.push(url);
|
||
}
|
||
});
|
||
|
||
// 设置到文件上传数据
|
||
if (fileUploadData[question.id]) {
|
||
fileUploadData[question.id].imageList = imageUrls.map(url => ({ url, name: url.split('/').pop() || '' }));
|
||
fileUploadData[question.id].fileList = fileUrls.map(url => ({ url, name: url.split('/').pop() || '' }));
|
||
}
|
||
}
|
||
} else if (question.questionType === 'VIDEO') {
|
||
// 视频类型:解析URL字符串
|
||
if (answerContent) {
|
||
const urls = answerContent.split(',').map((v: string) => v.trim()).filter((v: string) => v);
|
||
if (fileUploadData[question.id]) {
|
||
fileUploadData[question.id].videoList = urls.map(url => ({ url, name: url.split('/').pop() || '' }));
|
||
}
|
||
}
|
||
} else if (question.questionType === 'SIGNATURE') {
|
||
// 签名类型:直接使用base64字符串
|
||
answers[question.id].value = answerContent;
|
||
} else {
|
||
// 其他类型(TEXT, TEXTAREA, NUMBER, DATE, RADIO, SELECT):直接赋值
|
||
answers[question.id].value = answerContent;
|
||
}
|
||
}
|
||
});
|
||
|
||
// 如果有问卷级别的签名,也需要回显
|
||
// 注意:问卷级别的签名可能不在答案列表中,需要单独处理
|
||
// 这里暂时不处理,因为签名通常存储在单独的字段中
|
||
|
||
console.log('答案回显完成');
|
||
} else {
|
||
console.log('未找到已填写的答案');
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('加载已填写答案失败:', error);
|
||
// 加载失败不影响主流程,继续显示空白表单
|
||
}
|
||
};
|
||
|
||
// 重新填写问卷
|
||
const handleResubmit = async () => {
|
||
// 重置状态,允许重新填写
|
||
currentStep.value = 'fill';
|
||
// 重新初始化页面(会自动加载并回显已填写的答案)
|
||
await initializePage();
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.questionnaire-page {
|
||
min-height: 100vh;
|
||
background: #f5f5f5;
|
||
padding-bottom: 100rpx;
|
||
}
|
||
|
||
.loading-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100vh;
|
||
|
||
.loading-text {
|
||
margin-top: 20rpx;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
}
|
||
|
||
.submit-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 9999;
|
||
|
||
.submit-mask-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: white;
|
||
padding: 60rpx 80rpx;
|
||
border-radius: 16rpx;
|
||
|
||
.submit-mask-text {
|
||
margin-top: 30rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
.fill-step {
|
||
padding: 30rpx;
|
||
padding-bottom: 140rpx; // 为底部导航按钮留出空间
|
||
}
|
||
|
||
.questionnaire-header {
|
||
background: white;
|
||
padding: 30rpx;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
.questionnaire-title {
|
||
display: block;
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.questionnaire-desc {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
|
||
.questionnaire-info {
|
||
background: white;
|
||
padding: 20rpx 30rpx;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
.info-item {
|
||
display: flex;
|
||
margin-bottom: 12rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
min-width: 140rpx;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
flex: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
.questions-container {
|
||
.question-item {
|
||
background: white;
|
||
padding: 30rpx;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
transition: border-color 0.3s;
|
||
|
||
&.question-item-error {
|
||
border: 2px solid #F56C6C;
|
||
box-shadow: 0 0 0 2rpx rgba(245, 108, 108, 0.2);
|
||
}
|
||
|
||
.question-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 16rpx;
|
||
flex-wrap: wrap;
|
||
|
||
.question-required {
|
||
color: #F56C6C;
|
||
font-size: 28rpx;
|
||
margin-right: 4rpx;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.question-title {
|
||
font-size: 30rpx;
|
||
color: #333;
|
||
line-height: 1.6;
|
||
flex: 1;
|
||
|
||
.question-content-inline {
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.question-type-inline {
|
||
color: #666;
|
||
font-size: 26rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.question-input {
|
||
.input-field, .textarea-field {
|
||
width: 100%;
|
||
padding: 20rpx;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
background: #fafafa;
|
||
box-sizing: border-box;
|
||
transition: border-color 0.3s;
|
||
|
||
&.input-field-error, &.textarea-field-error {
|
||
border-color: #F56C6C;
|
||
background: #fff5f5;
|
||
}
|
||
}
|
||
|
||
.input-field {
|
||
height: 80rpx;
|
||
line-height: 40rpx;
|
||
}
|
||
|
||
.textarea-field {
|
||
min-height: 200rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.picker-view {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20rpx;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8rpx;
|
||
background: #fafafa;
|
||
transition: border-color 0.3s;
|
||
|
||
&.picker-view-error {
|
||
border-color: #F56C6C;
|
||
background: #fff5f5;
|
||
}
|
||
|
||
.picker-text {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.picker-placeholder {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.question-options {
|
||
padding: 10rpx;
|
||
border-radius: 8rpx;
|
||
transition: border-color 0.3s;
|
||
|
||
&.question-options-error {
|
||
border: 2px solid #F56C6C;
|
||
background: #fff5f5;
|
||
}
|
||
|
||
.option-item {
|
||
margin-bottom: 20rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.radio-wrapper, .checkbox-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.radio-circle, .checkbox-square {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border: 2px solid #ddd;
|
||
border-radius: 50%;
|
||
margin-right: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&.radio-selected, &.checkbox-selected {
|
||
border-color: #409EFF;
|
||
background: #409EFF;
|
||
}
|
||
|
||
.radio-inner {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
background: white;
|
||
border-radius: 50%;
|
||
}
|
||
}
|
||
|
||
.checkbox-square {
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.option-label {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.option-text {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
flex: 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.question-upload {
|
||
// ImageVideoUpload 组件自带样式,这里可以添加额外样式
|
||
padding: 10rpx;
|
||
border-radius: 8rpx;
|
||
transition: border-color 0.3s;
|
||
|
||
&.question-upload-error {
|
||
border: 2px solid #F56C6C;
|
||
background: #fff5f5;
|
||
}
|
||
}
|
||
|
||
.question-signature {
|
||
padding: 10rpx;
|
||
border-radius: 8rpx;
|
||
transition: border-color 0.3s;
|
||
|
||
&.question-signature-error {
|
||
border: 2px solid #F56C6C;
|
||
background: #fff5f5;
|
||
}
|
||
|
||
.signature-preview-inline {
|
||
.signature-preview-img-inline {
|
||
width: 100%;
|
||
min-height: 200rpx;
|
||
max-height: 400rpx;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8rpx;
|
||
background: #fafafa;
|
||
}
|
||
.signature-actions-inline {
|
||
margin-top: 16rpx;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
|
||
.signature-placeholder-inline {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 200rpx;
|
||
border: 2px dashed #e0e0e0;
|
||
border-radius: 8rpx;
|
||
background: #fafafa;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
|
||
&:active {
|
||
background: #f0f0f0;
|
||
border-color: #409EFF;
|
||
}
|
||
|
||
.signature-placeholder-text-inline {
|
||
margin-top: 16rpx;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.signature-section {
|
||
background: white;
|
||
padding: 30rpx;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
.signature-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
|
||
.signature-label {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.signature-required {
|
||
color: #F56C6C;
|
||
font-size: 28rpx;
|
||
margin-left: 4rpx;
|
||
}
|
||
}
|
||
|
||
.signature-preview {
|
||
.signature-preview-img {
|
||
width: 100%;
|
||
min-height: 200rpx;
|
||
max-height: 400rpx;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8rpx;
|
||
background: #fafafa;
|
||
}
|
||
.signature-actions {
|
||
margin-top: 16rpx;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
|
||
.signature-placeholder {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 200rpx;
|
||
border: 2px dashed #e0e0e0;
|
||
border-radius: 8rpx;
|
||
background: #fafafa;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
|
||
&:active {
|
||
background: #f0f0f0;
|
||
border-color: #409EFF;
|
||
}
|
||
|
||
.signature-placeholder-text {
|
||
margin-top: 16rpx;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.page-header {
|
||
background: white;
|
||
padding: 30rpx;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
text-align: center;
|
||
|
||
.page-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.page-progress {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
margin-left: 8rpx;
|
||
}
|
||
}
|
||
|
||
.page-navigation {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
display: flex;
|
||
background: white;
|
||
border-top: 1px solid #e0e0e0;
|
||
padding: 20rpx 30rpx;
|
||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||
z-index: 100;
|
||
|
||
.nav-btn {
|
||
flex: 1;
|
||
height: 80rpx;
|
||
line-height: 80rpx;
|
||
text-align: center;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
border: none;
|
||
border-radius: 8rpx;
|
||
margin: 0 10rpx;
|
||
|
||
&.prev-btn {
|
||
background: #f5f5f5;
|
||
color: #666;
|
||
}
|
||
|
||
&.next-btn, &.submit-btn {
|
||
background: #409EFF;
|
||
color: white;
|
||
}
|
||
|
||
&:first-child {
|
||
margin-left: 0;
|
||
}
|
||
|
||
&:last-child {
|
||
margin-right: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.submit-btn {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 100rpx;
|
||
background: #409EFF;
|
||
color: white;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
border: none;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.success-step, .no-permission-step, .already-filled-step, .time-invalid-step {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 100vh;
|
||
padding: 40rpx;
|
||
|
||
.success-content, .no-permission-content, .already-filled-content, .time-invalid-content {
|
||
text-align: center;
|
||
|
||
.success-title, .no-permission-title, .already-filled-title, .time-invalid-title {
|
||
display: block;
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin: 30rpx 0 16rpx;
|
||
}
|
||
|
||
.success-time {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.success-tip, .no-permission-subtitle, .already-filled-subtitle, .time-invalid-subtitle {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.ai-analyze-btn {
|
||
width: 320rpx;
|
||
height: 80rpx;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
border: none;
|
||
border-radius: 40rpx;
|
||
margin-bottom: 30rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.4);
|
||
transition: all 0.3s;
|
||
|
||
&:active {
|
||
transform: scale(0.96);
|
||
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.3);
|
||
}
|
||
}
|
||
|
||
.resubmit-btn {
|
||
width: 320rpx;
|
||
height: 80rpx;
|
||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||
color: white;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
border: none;
|
||
border-radius: 40rpx;
|
||
margin-bottom: 30rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 4rpx 12rpx rgba(245, 87, 108, 0.4);
|
||
transition: all 0.3s;
|
||
|
||
&:active {
|
||
transform: scale(0.96);
|
||
box-shadow: 0 2rpx 8rpx rgba(245, 87, 108, 0.3);
|
||
}
|
||
}
|
||
|
||
.back-btn {
|
||
width: 300rpx;
|
||
height: 80rpx;
|
||
background: #409EFF;
|
||
color: white;
|
||
font-size: 30rpx;
|
||
border: none;
|
||
border-radius: 40rpx;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|