1762 lines
53 KiB
Vue
1762 lines
53 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="!analysisResult && !analyzing" @click="analyzeWithAI" class="ai-analyze-btn">
|
|||
|
|
<u-icon name="file-text" size="18" color="#fff" style="margin-right: 8rpx;"></u-icon>
|
|||
|
|
AI智能分析
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<!-- 分析中状态 -->
|
|||
|
|
<view v-if="analyzing" class="analyzing-container">
|
|||
|
|
<u-loading-icon mode="spinner" size="24" color="#409EFF"></u-loading-icon>
|
|||
|
|
<text class="analyzing-text">AI分析中,请稍候...</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 分析结果 -->
|
|||
|
|
<view v-if="analysisResult" class="analysis-result">
|
|||
|
|
<view class="analysis-header">
|
|||
|
|
<u-icon name="file-text" size="20" color="#409EFF"></u-icon>
|
|||
|
|
<text class="analysis-title">AI分析报告</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="analysis-content">
|
|||
|
|
<text class="analysis-text">{{ analysisResult }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<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>
|
|||
|
|
<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,
|
|||
|
|
questionnaireAnswerFindByUserApi,
|
|||
|
|
questionnaireAnalyzeWithAIApi
|
|||
|
|
} 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 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);
|
|||
|
|
|
|||
|
|
// AI分析相关
|
|||
|
|
const analyzing = ref(false);
|
|||
|
|
const analysisResult = ref('');
|
|||
|
|
|
|||
|
|
// 签名组件相关
|
|||
|
|
const signCompRef = ref<any>(null);
|
|||
|
|
const signTitle = ref<string>("问卷签名");
|
|||
|
|
const sign_file = ref<string>("");
|
|||
|
|
const showSignature = ref<boolean>(false);
|
|||
|
|
const currentSignatureQuestionId = ref<string>(""); // 当前正在签名的题目ID
|
|||
|
|
|
|||
|
|
onLoad(async (params) => {
|
|||
|
|
options.value = params;
|
|||
|
|
openId.value = params?.openId || '';
|
|||
|
|
xxtsId.value = params?.id || ''; // 从URL参数id获取xxtsId
|
|||
|
|
|
|||
|
|
// 如果有 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;
|
|||
|
|
} 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 (questionnaireInfo.value.allowResubmit !== 1) {
|
|||
|
|
const answerResult = await questionnaireAnswerFindByUserApi({
|
|||
|
|
questionnaireId: questionnaireId.value
|
|||
|
|
});
|
|||
|
|
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: []
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 提交答案
|
|||
|
|
const result = await questionnaireAnswerSaveApi(submitParams);
|
|||
|
|
|
|||
|
|
if (result && result.resultCode === 1) {
|
|||
|
|
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 analyzeWithAI = async () => {
|
|||
|
|
try {
|
|||
|
|
analyzing.value = true;
|
|||
|
|
analysisResult.value = '';
|
|||
|
|
|
|||
|
|
// 构造发送给AI的问卷数据,包含所有分页的题目
|
|||
|
|
const allQuestions: any[] = [];
|
|||
|
|
pageList.value.forEach(page => {
|
|||
|
|
allQuestions.push(...page.questions);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const questionnaireData = {
|
|||
|
|
questionnaireId: questionnaireId.value,
|
|||
|
|
title: questionnaireInfo.value?.title,
|
|||
|
|
description: questionnaireInfo.value?.description,
|
|||
|
|
questions: allQuestions.map((q: any) => {
|
|||
|
|
let answerValue = answers[q.id]?.value;
|
|||
|
|
|
|||
|
|
// 处理不同类型的答案格式
|
|||
|
|
if (q.questionType === 'CHECKBOX' && Array.isArray(answerValue)) {
|
|||
|
|
answerValue = answerValue.join(',');
|
|||
|
|
} else if (q.questionType === 'FILE') {
|
|||
|
|
const uploadData = fileUploadData[q.id];
|
|||
|
|
const imageUrls = (uploadData?.imageList || []).filter(img => img.url).map(img => img.url);
|
|||
|
|
const fileUrls = (uploadData?.fileList || []).filter(file => file.url).map(file => file.url);
|
|||
|
|
answerValue = [...imageUrls, ...fileUrls].join(',');
|
|||
|
|
} else if (q.questionType === 'VIDEO') {
|
|||
|
|
const uploadData = fileUploadData[q.id];
|
|||
|
|
const videoUrls = (uploadData?.videoList || []).filter(video => video.url).map(video => video.url);
|
|||
|
|
answerValue = videoUrls.join(',');
|
|||
|
|
} else if (q.questionType === 'SIGNATURE') {
|
|||
|
|
answerValue = answerValue ? '[已签名]' : '[未签名]';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取分页名称和排序
|
|||
|
|
const pageName = q.pageName || '第一部分';
|
|||
|
|
const pageSortOrder = q.pageSortOrder !== undefined ? q.pageSortOrder : 0;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
content: q.questionContent,
|
|||
|
|
type: q.questionType,
|
|||
|
|
answer: answerValue || '[未填写]',
|
|||
|
|
pageName: pageName,
|
|||
|
|
pageSortOrder: pageSortOrder
|
|||
|
|
};
|
|||
|
|
})
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 调用后端AI分析接口
|
|||
|
|
const response = await questionnaireAnalyzeWithAIApi(questionnaireData);
|
|||
|
|
|
|||
|
|
if (response && response.resultCode === 1) {
|
|||
|
|
analysisResult.value = response.result || response.data || '分析完成';
|
|||
|
|
} else {
|
|||
|
|
throw new Error(response?.message || 'AI分析失败');
|
|||
|
|
}
|
|||
|
|
} catch (error: any) {
|
|||
|
|
console.error('AI分析失败:', error);
|
|||
|
|
uni.showToast({ title: error.message || 'AI分析失败,请稍后重试', icon: 'none', duration: 2000 });
|
|||
|
|
} finally {
|
|||
|
|
analyzing.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
</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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.analyzing-container {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
margin-bottom: 30rpx;
|
|||
|
|
padding: 30rpx;
|
|||
|
|
background: #f5f7fa;
|
|||
|
|
border-radius: 16rpx;
|
|||
|
|
|
|||
|
|
.analyzing-text {
|
|||
|
|
margin-left: 20rpx;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #409EFF;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.analysis-result {
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 16rpx;
|
|||
|
|
padding: 30rpx;
|
|||
|
|
margin-bottom: 30rpx;
|
|||
|
|
text-align: left;
|
|||
|
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
|||
|
|
max-width: 100%;
|
|||
|
|
|
|||
|
|
.analysis-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
padding-bottom: 16rpx;
|
|||
|
|
border-bottom: 2rpx solid #eee;
|
|||
|
|
|
|||
|
|
.analysis-title {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #333;
|
|||
|
|
margin-left: 12rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.analysis-content {
|
|||
|
|
.analysis-text {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #666;
|
|||
|
|
line-height: 1.8;
|
|||
|
|
word-break: break-word;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.back-btn {
|
|||
|
|
width: 300rpx;
|
|||
|
|
height: 80rpx;
|
|||
|
|
background: #409EFF;
|
|||
|
|
color: white;
|
|||
|
|
font-size: 30rpx;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 40rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|