前端——问卷系统评分题保存草稿报错的解决方案

问题背景

在开发问卷调查/满意度调查功能时,通常支持多种问题类型:

  • 单选题
  • 多选题
  • 评分题

当用户创建调查问卷,选择评分题类型后,点击保存草稿时出现报错。

问题复现

操作步骤:

  1. 进入满意度调查功能
  2. 点击"创建调查"
  3. 添加一个问题,类型选择"评分"
  4. 填写问题内容
  5. 点击"保存草稿"
  6. 结果:提示报错,保存失败

问题分析

通过代码分析,发现问题根源:

1. 数据模型不完整

评分题的数据结构只定义了 maxScore(最大分值),缺少 minScore(最小分值):

// 错误的数据结构{questionContent:"请对服务评分",voteType:3,// 3 = 评分题maxScore:5,// 缺少 minScore 字段!}

2. UI 控件缺失

页面模板中只有"最大分值"输入框,没有"最小分值"输入框。

3. 数据提交不完整

提交给后端的数据缺少 minScore 字段,导致后端校验失败。

4. 验证逻辑不全

表单验证只检查了 maxScore,没有验证 minScore 以及两者的关系。

解决方案

方案一:完善数据模型

在表单初始化时添加完整的评分字段:

// 问卷表单数据结构const surveyForm =ref({title:'',description:'',questions:[{questionContent:'',questionType:1,// 1=单选, 2=多选, 3=评分options:[{optionName:'',hasError:false}],// 评分题专用字段maxScore:5,// 最大分值minScore:1,// 最小分值(新增)maxScoreError:false,maxScoreErrorText:'',minScoreError:false,// 新增minScoreErrorText:'',// 新增questionError:false,},],});

方案二:添加最小分值 UI 控件

<template> <view v-for="(question, index) in surveyForm.questions" :key="index"> <!-- 问题内容 --> <view> <text>问题内容</text> <input v-model="question.questionContent" placeholder="请输入问题内容" :class="{ 'input-error': question.questionError }" /> </view> <!-- 问题类型选择 --> <view> <text>问题类型</text> <view> <view v-for="type in questionTypes" :key="type.value" :class="['type-option', { active: question.questionType === type.value }]" @click="setQuestionType(index, type.value)" > {{ type.label }} </view> </view> </view> <!-- 单选/多选的选项列表 --> <view v-if="question.questionType !== 3"> <view v-for="(option, optIndex) in question.options" :key="optIndex" > <input v-model="option.optionName" placeholder="请输入选项内容" :class="{ 'input-error': option.hasError }" /> <button @click="removeOption(index, optIndex)">删除</button> </view> <button @click="addOption(index)">添加选项</button> </view> <!-- 评分题配置 --> <view v-if="question.questionType === 3"> <!-- 最大分值 --> <view> <text>最大分值:</text> <input type="number" v-model.number="question.maxScore" :class="{ 'input-error': question.maxScoreError }" placeholder="请输入最大分值(1-10)" @blur="handleMaxScoreChange($event, index)" @input="clearMaxScoreError(index)" /> <text v-if="question.maxScoreError"> {{ question.maxScoreErrorText }} </text> </view> <!-- 最小分值(新增) --> <view> <text>最小分值:</text> <input type="number" v-model.number="question.minScore" :class="{ 'input-error': question.minScoreError }" placeholder="请输入最小分值(1-10)" @blur="handleMinScoreChange($event, index)" @input="clearMinScoreError(index)" /> <text v-if="question.minScoreError"> {{ question.minScoreErrorText }} </text> </view> <!-- 评分预览 --> <view> <text>评分范围预览:{{ question.minScore }} - {{ question.maxScore }} 分</text> </view> </view> </view> </template> <script setup> import { ref } from 'vue'; const questionTypes = [ { label: '单选', value: 1 }, { label: '多选', value: 2 }, { label: '评分', value: 3 }, ]; const surveyForm = ref({ title: '', description: '', questions: [createEmptyQuestion()], }); // 创建空问题 function createEmptyQuestion() { return { questionContent: '', questionType: 1, options: [{ optionName: '', hasError: false }], maxScore: 5, minScore: 1, maxScoreError: false, maxScoreErrorText: '', minScoreError: false, minScoreErrorText: '', questionError: false, }; } // 设置问题类型 const setQuestionType = (questionIndex, type) => { const question = surveyForm.value.questions[questionIndex]; question.questionType = type; if (type === 3) { // 评分题:设置默认分值,清空选项 question.maxScore = 5; question.minScore = 1; question.maxScoreError = false; question.maxScoreErrorText = ''; question.minScoreError = false; question.minScoreErrorText = ''; question.options = []; } else { // 非评分题:确保有选项 if (!question.options.length) { question.options = [{ optionName: '', hasError: false }]; } } }; // 清除最大分值错误 const clearMaxScoreError = (questionIndex) => { const question = surveyForm.value.questions[questionIndex]; question.maxScoreError = false; question.maxScoreErrorText = ''; }; // 清除最小分值错误 const clearMinScoreError = (questionIndex) => { const question = surveyForm.value.questions[questionIndex]; question.minScoreError = false; question.minScoreErrorText = ''; }; // 处理最大分值变化 const handleMaxScoreChange = (event, questionIndex) => { const question = surveyForm.value.questions[questionIndex]; const value = parseInt(event.detail?.value || event.target?.value, 10); // 清除错误状态 question.maxScoreError = false; question.maxScoreErrorText = ''; // 验证 if (isNaN(value) || value < 1) { question.maxScoreError = true; question.maxScoreErrorText = '请输入大于0的整数'; return; } if (value > 10) { question.maxScoreError = true; question.maxScoreErrorText = '最大值不能超过10'; question.maxScore = 10; return; } // 验证最大值必须大于最小值 if (question.minScore && value <= question.minScore) { question.maxScoreError = true; question.maxScoreErrorText = '最大分值必须大于最小分值'; return; } question.maxScore = value; }; // 处理最小分值变化 const handleMinScoreChange = (event, questionIndex) => { const question = surveyForm.value.questions[questionIndex]; const value = parseInt(event.detail?.value || event.target?.value, 10); // 清除错误状态 question.minScoreError = false; question.minScoreErrorText = ''; // 验证 if (isNaN(value) || value < 1) { question.minScoreError = true; question.minScoreErrorText = '请输入大于0的整数'; return; } if (value > 10) { question.minScoreError = true; question.minScoreErrorText = '最大值不能超过10'; question.minScore = 10; return; } // 验证最小值必须小于最大值 if (question.maxScore && value >= question.maxScore) { question.minScoreError = true; question.minScoreErrorText = '最小分值必须小于最大分值'; return; } question.minScore = value; }; </script> 

方案三:完善表单验证逻辑

// 表单验证函数constvalidateForm=()=>{const errors =[];let firstErrorField =null;// 验证标题if(!surveyForm.value.title.trim()){ errors.push('title');if(!firstErrorField) firstErrorField ='title';}// 验证每个问题 surveyForm.value.questions.forEach((question, index)=>{// 验证问题内容if(!question.questionContent.trim()){ question.questionError =true; errors.push(`question_${index}_content`);if(!firstErrorField) firstErrorField =`question_${index}_content`;}else{ question.questionError =false;}// 根据问题类型验证if(question.questionType ===3){// 评分题验证validateScoreQuestion(question, index, errors, firstErrorField);}else{// 选择题验证validateOptionsQuestion(question, index, errors, firstErrorField);}});return{isValid: errors.length ===0, errors, firstErrorField,};};// 验证评分题constvalidateScoreQuestion=(question, index, errors, firstErrorField)=>{// 验证最大分值if( question.maxScore ===undefined|| question.maxScore ===null|| question.maxScore ===''|| question.maxScore <1|| question.maxScore >10){ question.maxScoreError =true; question.maxScoreErrorText ='请输入1-10的整数'; errors.push(`question_${index}_maxScore`);if(!firstErrorField) firstErrorField =`question_${index}_maxScore`;}else{ question.maxScoreError =false; question.maxScoreErrorText ='';}// 验证最小分值if( question.minScore ===undefined|| question.minScore ===null|| question.minScore ===''|| question.minScore <1|| question.minScore >10){ question.minScoreError =true; question.minScoreErrorText ='请输入1-10的整数'; errors.push(`question_${index}_minScore`);if(!firstErrorField) firstErrorField =`question_${index}_minScore`;}else{ question.minScoreError =false; question.minScoreErrorText ='';}// 验证分值范围关系if(!question.maxScoreError &&!question.minScoreError && question.maxScore <= question.minScore ){ question.maxScoreError =true; question.maxScoreErrorText ='最大分值必须大于最小分值'; errors.push(`question_${index}_scoreRange`);if(!firstErrorField) firstErrorField =`question_${index}_maxScore`;}};// 验证选择题constvalidateOptionsQuestion=(question, index, errors, firstErrorField)=>{if(!question.options || question.options.length <2){ errors.push(`question_${index}_options`);if(!firstErrorField) firstErrorField =`question_${index}_options`;return;} question.options.forEach((option, optIndex)=>{if(!option.optionName.trim()){ option.hasError =true; errors.push(`question_${index}_option_${optIndex}`);if(!firstErrorField) firstErrorField =`question_${index}_option_${optIndex}`;}else{ option.hasError =false;}});};

方案四:完善数据提交逻辑

// 收集表单数据constcollectFormData=()=>{return{title: surveyForm.value.title,description: surveyForm.value.description,questions: surveyForm.value.questions.map((question)=>({questionContent: question.questionContent,questionType: question.questionType,// 选择题提交选项options: question.questionType !==3? question.options.map((opt)=>({optionName: opt.optionName })):[],// 评分题提交分值范围(关键修复点)maxScore: question.questionType ===3? question.maxScore :undefined,minScore: question.questionType ===3? question.minScore :undefined,})),};};// 保存草稿constsaveDraft=async()=>{try{// 验证表单const{ isValid, firstErrorField }=validateForm();if(!isValid){showToast('请完善表单信息');// 滚动到第一个错误字段scrollToError(firstErrorField);return;}showLoading('保存中...');// 收集数据const formData =collectFormData();// 调用保存草稿接口const result =awaitsaveSurveyDraft({...formData,isDraft:true,});hideLoading();if(result.success){showToast('草稿保存成功');// 可选:返回列表页// navigateBack();}else{showToast(result.message ||'保存失败');}}catch(error){hideLoading(); console.error('保存草稿失败:', error);showToast('保存失败,请重试');}};// 提交调查constsubmitSurvey=async()=>{try{const{ isValid, firstErrorField }=validateForm();if(!isValid){showToast('请完善表单信息');scrollToError(firstErrorField);return;}showLoading('提交中...');const formData =collectFormData();const result =awaitcreateSurvey({...formData,isDraft:false,});hideLoading();if(result.success){showToast('创建成功');navigateBack();}else{showToast(result.message ||'创建失败');}}catch(error){hideLoading();showToast('提交失败,请重试');}};

方案五:加载草稿时的数据处理

// 加载草稿数据constloadDraftData=async(draftId)=>{try{showLoading('加载中...');const response =awaitgetSurveyDetail({id: draftId });hideLoading();if(response.success && response.data){const data = response.data;// 填充表单数据 surveyForm.value.title = data.title ||''; surveyForm.value.description = data.description ||'';// 处理问题列表,确保字段完整 surveyForm.value.questions =(data.questions ||[]).map((question)=>({questionContent: question.questionContent ||'',questionType: question.questionType ||1,options:(question.options ||[]).map((opt)=>({optionName: opt.optionName ||'',hasError:false,})),// 关键:确保评分题有默认的最大/最小分值maxScore: question.maxScore ??(question.questionType ===3?5:undefined),minScore: question.minScore ??(question.questionType ===3?1:undefined),maxScoreError:false,maxScoreErrorText:'',minScoreError:false,minScoreErrorText:'',questionError:false,}));// 如果没有问题,添加一个默认问题if(surveyForm.value.questions.length ===0){ surveyForm.value.questions.push(createEmptyQuestion());}}}catch(error){hideLoading();showToast('加载失败');}};

完整数据结构对比

修复前(不完整)

// 评分题数据 - 缺少 minScore{questionContent:"请对服务评分",questionType:3,options:[],maxScore:5// minScore 字段缺失,导致后端校验失败}

修复后(完整)

// 评分题数据 - 包含完整的分值范围{questionContent:"请对服务评分",questionType:3,options:[],maxScore:5,minScore:1// 新增:最小分值}

最佳实践总结

1. 数据模型设计要完整

设计数据结构时,考虑所有必要字段:

  • 评分题需要 minScoremaxScore
  • 为字段设置合理的默认值

2. UI 与数据模型保持一致

数据模型有的字段,UI 也要有对应的控件让用户填写。

3. 验证逻辑要全面

  • 验证单个字段的有效性
  • 验证字段之间的关系(如 maxScore > minScore)
  • 提供友好的错误提示

4. 数据提交前再次检查

collectFormData 时确保所有必要字段都被正确收集。

5. 加载数据时设置默认值

从后端加载数据时,使用 ??|| 为可能缺失的字段设置默认值。

// 使用 nullish coalescing 设置默认值maxScore: data.maxScore ??5,minScore: data.minScore ??1,

常见问题类型数据结构参考

// TypeScript 类型定义interfaceQuestion{ questionContent:string; questionType:1|2|3;// 1=单选, 2=多选, 3=评分// 选择题字段 options?:Array<{ optionName:string; hasError?:boolean;}>;// 评分题字段 maxScore?:number; minScore?:number;// 错误状态 questionError?:boolean; maxScoreError?:boolean; maxScoreErrorText?:string; minScoreError?:boolean; minScoreErrorText?:string;}interfaceSurvey{ id?:string; title:string; description:string; questions: Question[]; isDraft:boolean; createTime?:string; updateTime?:string;}

关键词:问卷调查、评分题、数据模型、表单验证、草稿保存

适用场景:满意度调查、问卷系统、投票功能、评价系统等需要评分类型问题的业务场景

Read more

提升文档处理效率!DeepSeek-OCR-WebUI实现批量识别与精准定位

提升文档处理效率!DeepSeek-OCR-WebUI实现批量识别与精准定位 1. 引言:从命令行到可视化,OCR应用的工程化跃迁 在人工智能驱动办公自动化的浪潮中,光学字符识别(OCR)技术正成为连接物理文档与数字世界的桥梁。尽管许多OCR模型具备强大的文本识别能力,但缺乏直观交互界面的传统推理脚本严重制约了其在实际业务场景中的落地效率。 DeepSeek-OCR-WebUI 的出现填补了这一空白。作为基于 DeepSeek 开源 OCR 大模型构建的 Web 应用,它不仅封装了底层复杂的推理逻辑,更通过现代化 UI 设计实现了“上传即识别”的极简操作体验。尤其在金融票据处理、教育资料数字化、档案管理等需要高精度文本提取和位置定位的领域,该工具展现出显著的生产力提升价值。 本文将围绕 DeepSeek-OCR-WEBUI 镜像展开,系统介绍其核心功能特性、部署流程及典型应用场景,重点解析如何利用其批量处理能力和精准定位模式提升文档自动化水平。 2. 核心功能深度解析 2.1 七大识别模式:按需选择,精准匹配业务需求 DeepSeek-OCR-WebUI 最具差异化的

ClawdBot开发者工具:ClawdBot CLI + Webhook + 自定义Agent扩展实践

ClawdBot开发者工具:ClawdBot CLI + Webhook + 自定义Agent扩展实践 ClawdBot 不是一个云端服务,而是一个真正属于你自己的 AI 助手运行时环境。它不依赖外部 API 密钥,不上传用户数据,所有推理、记忆、工作流都在本地完成。你可以把它理解为「AI 助手的操作系统」——提供统一的命令行界面、可编程的事件通道、模块化的智能体架构,以及面向开发者的完整扩展能力。 它背后的核心推理引擎是 vLLM,一个高性能、低延迟的大模型服务框架。这意味着你不需要从零搭建模型服务,ClawdBot 已将 vLLM 封装为开箱即用的后端能力,支持 Qwen、Llama、Phi 等主流开源模型,且能充分利用 GPU 显存与计算资源。更重要的是,它不是单点工具,而是一套可组合、可嵌入、可演进的开发者工具链:CLI 是你的控制台,Webhook 是它的神经末梢,

零基础学微信小程序前端(原生JS):从0到1写第一个可交互页面

零基础学微信小程序前端(原生JS):从0到1写第一个可交互页面

目录 一、小程序前端的核心差异 二、前期准备:微信开发者工具搭建 三、核心知识点:小程序前端的目录结构 四、实操:写第一个可交互页面 1. 编写页面结构(index.wxml) 2. 编写页面样式(index.wxss) 3. 编写页面逻辑(index.js) 五、运行测试:看看效果 六、新手常见问题&解决方法 七、入门总结 一、小程序前端的核心差异 和你熟悉的 Web 前端(HTML+CSS+JS)相比,小程序有 3 个核心不同: 1. 标签不同:HTML 的div/p/

Z-Image-Turbo输出格式限制:PNG转JPG/WEBP后处理方案

Z-Image-Turbo输出格式限制:PNG转JPG/WEBP后处理方案 你是不是也遇到过这样的烦恼?用Z-Image-Turbo生成了一张特别满意的图片,想分享到社交媒体或者用在网页上,结果发现文件太大了——一张1024×1024的PNG图片,动不动就几兆甚至十几兆,加载慢不说,还特别占存储空间。 更让人头疼的是,很多平台对上传的图片格式和大小都有严格限制。微信朋友圈上传大图会压缩得惨不忍睹,网站上传大文件又慢又容易失败。难道每次生成完图片,还得手动用Photoshop或者在线工具转换格式、压缩大小吗? 今天我就来分享一个简单实用的解决方案:为Z-Image-Turbo添加自动后处理功能,让生成的PNG图片自动转换成更轻量的JPG或WEBP格式,还能智能压缩,保持画质的同时大幅减小文件体积。 1. 为什么需要后处理转换? 1.1 PNG格式的优缺点 先说说Z-Image-Turbo默认输出的PNG格式。PNG是个好格式,它支持透明背景,采用无损压缩,画质保持得非常好。但问题也在这里: * 文件体积大:同样一张1024×1024的图片,PNG格式可能5-10MB,而