HarmonyOS7问卷调查创建器实战:题目组织与统计预览

📅 2026/7/14 5:29:29
HarmonyOS7问卷调查创建器实战:题目组织与统计预览
文章目录完整代码最后总结问卷页面表面上是在渲染几道题实际更像一个小型表单引擎。单选、多选、评分、文本输入每种题型的交互都不一样但它们又必须被同一个列表承载还要能在填写、预览、结果之间切换。这个案例的重点就在这里用一份题目配置驱动多种 UI 形态。SurveyQuestion_v8en是整页的核心模型。每道题都有type、title、options、required同时还带着用户当前输入状态userAnswer、userText、scaleValue。这说明它不是纯粹的题目定义也不是纯粹的答题结果而是把题目配置和当前作答状态放在了一起。这种写法在 demo 里很直接读起来也容易。正式项目里我可能会把“题目配置”和“用户答案”拆成两份数据避免服务端题库和本地作答状态混在一起。但作为教学案例它能很好地展示一个配置驱动页面的基本思路题型决定渲染方式答案状态决定选中效果。mode是另一个关键状态。它有fill、preview、result三种语义决定页面当前是填写态、预览态还是结果态。这里最有意思的是同一份questions在不同mode下会长成不同界面填写态显示可点击选项结果态显示票数、百分比和进度条。数据没换解释方式换了。这类页面如果没有mode很容易写成三套页面一个填写页、一个预览页、一个结果页。短期看也能跑长期会很麻烦因为题目标题、选项结构、必填标识这些东西会被复制三遍。当前写法把模式当成状态让渲染逻辑围绕同一份题目数据展开扩展成本会低一些。toggleAnswer(questionId, optionId, isSingle)是这页最应该认真看的函数。它先通过questionId找到目标题目再根据isSingle决定答案更新方式。单选题直接把userAnswer替换成[optionId]多选题则判断当前选项是否已经存在存在就移除不存在就追加。这段逻辑虽然短但已经覆盖了问卷里最常见的选择题状态机。单选是互斥选择多选是集合增删。把这两种规则收进同一个函数之后UI 层只需要在点击选项时传入题目 id、选项 id 和题型判断不需要在每个选项里重复写一遍选择规则。不过这里也有一个值得注意的实现细节函数内部在map()里直接修改了q.userAnswer。示例代码能工作但如果题目结构继续复杂或者拆成子组件后依赖引用比较我会更倾向于返回新对象。问卷编辑器这种页面状态嵌套比较深不可变更新能减少很多难排查的问题。评分题和文本题没有走toggleAnswer()而是在各自组件的onChange里更新scaleValue和userText。这也合理因为它们不是选项集合而是连续值和自由文本。比较好的地方是这两种输入仍然回写到questions这份主状态里所以提交、预览、结果切换时不用再去别的地方找答案。结果模式的处理也比较直观。选项题在mode result时不再展示可点击行而是展示count、百分比和Progress。getTotalResponses()现在直接返回123相当于模拟总样本量。真实业务里这个值应该来自接口而且还要注意不同题目可能有不同作答人数尤其是非必填题不能简单共用一个总数。submitted的职责比较单一控制是否已经提交以及是否显示感谢反馈。点击“提交问卷”后代码把submitted设为true同时把mode切到result。这条路径很清楚用户从填写态进入结果态页面底部也出现反馈提示。对于 demo 来说这已经能表达完整闭环。如果把这个问卷继续往真实产品推进我会优先补校验。现在required只是展示红色星号并没有阻止用户空着提交。更完整的逻辑应该在提交前遍历questions必填单选/多选必须有userAnswer必填文本必须有userText评分题要确认值是否有效。校验结果最好也回到题目卡片上而不是只弹一个全局提示。这篇案例真正能复用的不是某个 UI 片段而是配置驱动的组织方式。questions描述题目type决定渲染分支mode决定填写还是结果toggleAnswer()处理选择题状态更新。只要这几层关系清楚后面加日期题、图片题、排序题或者接入远程问卷配置都有地方放。完整代码下面保留案例完整代码方便你直接对照学习、复制和重构。// 问卷调查创建器: 问卷调查创建器 - 题目类型多样、预览模式、结果统计interfaceSurveyOption_pknu{id:numbertext:stringcount:number}interfaceSurveyQuestion_v8en{id:numbertype:string// single | multi | text | scaletitle:stringoptions:SurveyOption_pknu[]required:booleanuserAnswer:number[]userText:stringscaleValue:number}EntryComponentstruct SurveyBuilderWorkbench{Statequestions:SurveyQuestion_v8en[][{id:1,type:single,title:您最常使用哪类应用,required:true,options:[{id:1,text:社交娱乐,count:45},{id:2,text:工具效率,count:32},{id:3,text:购物电商,count:28},{id:4,text:新闻资讯,count:18},],userAnswer:[],userText:,scaleValue:3,},{id:2,type:multi,title:您希望应用具备哪些特性可多选,required:true,options:[{id:1,text:界面美观,count:67},{id:2,text:操作便捷,count:89},{id:3,text:功能丰富,count:45},{id:4,text:隐私安全,count:72},{id:5,text:性能流畅,count:91},],userAnswer:[],userText:,scaleValue:3,},{id:3,type:scale,title:您对当前移动应用生态的满意度,required:false,options:[],userAnswer:[],userText:,scaleValue:3,},{id:4,type:text,title:您对未来应用有哪些期待,required:false,options:[],userAnswer:[],userText:,scaleValue:3,},]Statemode:stringfill// fill | preview | resultStatesubmitted:booleanfalsegetTotalResponses():number{return123}toggleAnswer(questionId:number,optionId:number,isSingle:boolean){this.questionsthis.questions.map((q:SurveyQuestion_v8en){if(q.id!questionId)returnqif(isSingle){q.userAnswer[optionId]returnq}else{constansq.userAnswer.includes(optionId)?q.userAnswer.filter(aa!optionId):q.userAnswer.concat([optionId])q.userAnsweransreturnq}})}build(){Column(){// 顶部Row(){Text(问卷调查).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#1A1A1A)Blank()Row({space:8}){Text(this.modefill?预览:填写).fontSize(13).fontColor(#007AFF).onClick((){this.modethis.modefill?preview:fill})Text(结果).fontSize(13).fontColor(#FF9500).onClick((){this.moderesult})}}.padding({left:20,right:20,top:20,bottom:8})// 标题区Column({space:4}){Text(用户使用习惯调查).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#1A1A1A)Text(帮助我们了解您的需求提供更好的服务体验).fontSize(12).fontColor(#888888)Row(){Text(\共 \${this.questions.length}题\).fontSize(11).fontColor(#888888)Text( · ).fontSize(11).fontColor(#CCCCCC)Text(\\${this.getTotalResponses()}人已填写\).fontSize(11).fontColor(#888888)}.margin({top:4})}.width(100%).padding(16).backgroundColor(#FFFFFF).alignItems(HorizontalAlign.Start).margin({bottom:8})Scroll(){Column({space:12}){ForEach(this.questions,(question:SurveyQuestion_v8en,qIdx:number){Column({space:12}){Row(){Text(\\${qIdx1}\).fontSize(13).fontWeight(FontWeight.Bold).fontColor(#FFFFFF).backgroundColor(#007AFF).width(24).height(24).textAlign(TextAlign.Center).lineHeight(24).borderRadius(12)Text(question.title).fontSize(14).fontColor(#1A1A1A).fontWeight(FontWeight.Medium).layoutWeight(1).margin({left:8})if(question.required){Text(*).fontSize(14).fontColor(#FF3B30).margin({left:4})}}.width(100%)if(question.typesingle||question.typemulti){Column({space:8}){ForEach(question.options,(opt:SurveyOption_pknu){if(this.moderesult){// 结果展示Column({space:4}){Row(){Text(opt.text).fontSize(13).fontColor(#333333).layoutWeight(1)Text(\\${opt.count}票\).fontSize(12).fontColor(#888888)Text(\\${Math.round(opt.count/this.getTotalResponses()*100)}%\).fontSize(12).fontColor(#007AFF).width(40).textAlign(TextAlign.End)}Progress({value:opt.count,total:this.getTotalResponses(),type:ProgressType.Linear}).width(100%).height(8).color(#007AFF).backgroundColor(#E5E5E5).borderRadius(4)}}else{Row(){Text(question.userAnswer.includes(opt.id)?(question.typesingle?:✅):⭕).fontSize(18)Text(opt.text).fontSize(13).fontColor(#333333).margin({left:10})}.width(100%).padding({top:10,bottom:10,left:12,right:12}).backgroundColor(question.userAnswer.includes(opt.id)?#EEF6FF:#F8F8F8).borderRadius(10).border({width:1.5,color:question.userAnswer.includes(opt.id)?#007AFF:transparent}).onClick(()this.toggleAnswer(question.id,opt.id,question.typesingle))}})}}elseif(question.typescale){Column({space:8}){Row(){Text(非常不满意).fontSize(11).fontColor(#888888)Blank()Text(非常满意).fontSize(11).fontColor(#888888)}.width(100%)Slider({value:question.scaleValue,min:1,max:5,step:1}).width(100%).selectedColor(#007AFF).trackColor(#E0E0E0).blockColor(#007AFF).onChange((val:number){this.questionsthis.questions.map((q){if(q.idquestion.id){q.scaleValueval}returnq})})Row(){ForEach([1,2,3,4,5],(n:number){Text(String(n)).fontSize(13).fontWeight(question.scaleValuen?FontWeight.Bold:FontWeight.Normal).fontColor(question.scaleValuen?#007AFF:#AAAAAA).layoutWeight(1).textAlign(TextAlign.Center)})}.width(100%)}}elseif(question.typetext){TextArea({text:question.userText,placeholder:请输入您的想法...}).height(100).borderRadius(10).backgroundColor(#F8F8F8).fontSize(13).onChange((v:string){this.questionsthis.questions.map((q){if(q.idquestion.id){q.userTextv}returnq})})}}.width(100%).padding(14).backgroundColor(#FFFFFF).borderRadius(14)})if(this.modefill!this.submitted){Button(提交问卷).width(100%).height(50).fontSize(16).fontWeight(FontWeight.Bold).fontColor(#FFFFFF).backgroundColor(#007AFF).borderRadius(25).margin({top:8}).onClick((){this.submittedtrue;this.moderesult})}if(this.submitted){Column(){Text( 感谢您的参与).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#34C759)Text(您的反馈对我们非常重要).fontSize(13).fontColor(#888888).margin({top:4})}.width(100%).padding(20).backgroundColor(#EEFBF2).borderRadius(14).alignItems(HorizontalAlign.Center)}}.padding({left:16,right:16,bottom:30})}.layoutWeight(1).scrollBar(BarState.Off)}.width(100%).height(100%).backgroundColor(#F2F2F7)}}最后总结问卷调查创建器 这个案例并不靠复杂技巧取胜它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后你再去写同类型功能速度会明显快很多。