vue3实现问卷调查的示例代码
作者:zt_ever
前言
问卷调查,这个东西真的随处可见,那不如自己做一个问卷调查?话不多说,我们来实现它!!!
我们需要实现的效果图如下:
开发工具
vscode(里面预先装好vue)
思路准备
通过分析调查问卷的功能,我们来梳理一下实现它的方式:
- 首先我们把这个问卷调查分为三个板块:首页(home),答题页(item),得分页(score)。实现这三个板块的跳转采用路由的方式。
- 我们可以看到 首页(home) 和 答题页(item) 中每道题目的切换都只是中间部分发生变化,其他的地方并没有改变,那么我们可以把其作为一个组件分离出去,在这个组件中完成代码编写。
- 最后单独编写得分页(score)。
一、创建vue3项目
我们使用vue create xxx
命令创建这个项目,我这以happy命名,并安装好路由,less预处理器。
二、构建目录结构
创建完毕后,我们对这些文件夹做进一步操作:
assets
:
1.创建images
文件夹放置图片
2.创建style
文件夹,在其中创建common.less
,让html5常用的标签初始化
components
:
1.创建item.vue
作为组件
mock
:
1.创建index.js
,其中存放后端数据( 这里采用静态的数据 )
router
:
1.创建index.js
,这是对路由的配置
utils
:
1.创建rem.js
,为了让用户在不同设备上有更好的查看效果,这里做了一个适配
views
:
1.创建home
文件夹,其中放置 index.vue
用于首页的页面编写
2.创建item
文件夹,其中放置 item.vue
用于答题页的页面编写
3.创建score
文件夹,其中放置 score.vue
用于得分页的页面编写
另外我们需在App.vue
中的template中添加router-view:
<template> <router-view/> </template> <style lang="less"> </style>
以及在main.js
中引入必要的文件:
import { createApp } from 'vue' import App from './App.vue' import router from './router' import '@/utils/rem.js' import '@/assets/style/common.less' createApp(App).use(router).mount('#app')
三、代码编写
1. 屏幕适配(rem.js)
(function (doc) { let docEl = doc.documentElement doc.addEventListener('DOMContentLoaded', () => { let clientWidth = docEl.clientWidth //获取屏幕宽度 docEl.style.fontSize = 20 * (clientWidth / 320) + 'px' //让1rem=20px }) })(document)
2. 标签初始化(common.less)
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; } html{ height: 100%; width: 100%; } body{ height: 100%; width: 100%; background: url(../images/1-1.jpg) no-repeat; //设置背景图片 background-size: 100% 100%; } .clear:after{ content: ''; display: block; clear: both; } .clear{ zoom:1; } .back_img{ background-repeat: no-repeat; background-size: 100% 100%; } .margin{ margin: 0 auto; } .left{ float: left; } .right{ float:right; } .hide{ display: none; } .show{ display: block; }
3. 后端数据(mock/index.js)
export const questions=[ { "topic_id": 20, "active_topic_id": 4, "type": "ONE", "topic_name": "题目一", "active_id": 1, "active_title": "欢乐星期五标题", "active_topic_phase": "第一周", "active_start_time": "1479139200", "active_end_time": "1482163200", "topic_answer": [{ "topic_answer_id": 1, "topic_id": 20, "answer_name": "答案aaaa", "is_standard_answer": 0 }, { "topic_answer_id": 2, "topic_id": 20, "answer_name": "答案bbbb", "is_standard_answer": 0 }, { "topic_answer_id": 3, "topic_id": 20, "answer_name": "答案cccc", "is_standard_answer": 0 }, { "topic_answer_id": 4, "topic_id": 20, "answer_name": "正确答案", "is_standard_answer": 1 }] }, { "topic_id": 21, "active_topic_id": 4, "type": "MORE", "topic_name": "题目二", "active_id": 1, "active_title": "欢乐星期五标题", "active_topic_phase": "第一周", "active_start_time": "1479139200", "active_end_time": "1482163200", "topic_answer": [{ "topic_answer_id": 5, "topic_id": 21, "answer_name": "正确答案", "is_standard_answer": 1 }, { "topic_answer_id": 6, "topic_id": 21, "answer_name": "答案B", "is_standard_answer": 0 }, { "topic_answer_id": 7, "topic_id": 21, "answer_name": "答案C", "is_standard_answer": 0 }, { "topic_answer_id": 8, "topic_id": 21, "answer_name": "答案D", "is_standard_answer": 0 }] } //后面数据省略,数据条数可多条,看题目量 ]
4. 组件编写(item.vue)
由于首页和答题页拥有共同部分,我们把其作为组件单独拿出来编写,那么在home页面和item页面中只需引入这个组件即可。
(1)首先编写组件中的首页:
思路:
首页分为图片背景,云朵里的第一周,开始按钮(用路由跳转)
<template> <section> <header class="top_tips"> <span class="num_tip">第一周</span> </header> <!-- 首页 --> <div class="home_logo item_container_style"> <router-link to="/item" class="start button_style"></router-link> <!-- 路由跳转至答题页面 --> </div> </section> </template>
(2)编写组件中的答题页:
思路:
- 用props实现父子组件的通信,v-if用来判断主页面和答题页面谁显示在页面上。
- 用index记录选项的下标,若选中的下标等于这个选项的下标即添加这个样式,将其变为黄色。
- 点击下一题按钮时未选择选项,需弹出提示框。
- 后端数据的选项的topic_answer_id是唯一值,可以利用这个特点将选中的所有选项的id值保存在result数组中,以便之后判断是否为正确答案计算得分。
- 当答题为最后一题时,按钮变为提交按钮,为其绑定点击事件,利用router的push方法实现路由传参和跳转,参数为存放用户选择答案的数组result。
- 点击提交按钮时,最后一题还未选择,需弹出提示框。
<template> <section> <header class="top_tips"> <span class="num_tip" v-if="fatherComponent === 'home'">第一周</span> <span class="num_tip" v-if="fatherComponent === 'item'">{{ ques[itemNum].topic_name }}</span> </header> <!-- 首页 --> <div v-if="fatherComponent === 'home'"> <div class="home_logo item_container_style"> <router-link to="/item" class="start button_style"></router-link> <!-- 路由跳转至答题页面 --> </div> </div> <!-- item答题页面 --> <div v-if="fatherComponent === 'item'"> <div class="item_back item_container_style"> <div class="item_list_container" v-if="ques && ques.length > 0"> <!-- 题目 --> <header class="item_title">{{ ques[itemNum].topic_name }}</header> <!-- 选项 --> <ul> <li class="item_list" @click="choosed(item.topic_answer_id, index)" v-for="(item, index) in ques[itemNum].topic_answer" :key="index"> <!-- 双向绑定一个类名,这个类名可修改选中的样式 --> <span class="option_style" :class="{ 'has_choosed': chooseNum === index }">{{ chooseType(index) }}</span> <span class="option_detail">{{ item.answer_name }}</span> </li> </ul> </div> </div> <!-- 下一题按钮 到倒数第二题这个按钮就不出现--> <span class="next_item button_style" @click="nextItem" v-if="itemNum < ques.length - 1"></span> <!-- 提交按钮 倒数第一题时出现--> <span class="submit_item button_style" @click="submitItem" v-else></span> </div> </section> </template> <script> import { questions } from '@/mock' import { ref } from 'vue'; import { useRouter } from 'vue-router' export default { props: { fatherComponent: String }, setup(props, context) { const ques = ref(questions) console.log(questions); let chooseNum = ref(null) //选中的答案 let itemNum = ref(0) //第几题 let result = [] //记录用户选中的答案 const chooseType = (type) => { //选项 switch (type) { case 0: return 'A'; case 1: return 'B'; case 2: return 'C'; case 3: return 'D'; } } const choosed = (id, index) => { //选中的id号push进result数组 console.log(index); chooseNum.value = index result.push(id) } const nextItem = () => { //下一题 if (chooseNum.value == null) { alert('你还没有选择') return } //切换题目数据 console.log(result); itemNum.value++ chooseNum.value = null //切换题目后将选中的选项置为空(不选中) } //提交 const router = useRouter() const submitItem = () => { if (chooseNum.value == null) { alert('你还没有选择') return } //跳去score页面 router.push({path:'/score',query:{answer:result}}) } return { choosed, chooseNum, itemNum, ques, chooseType, nextItem, submitItem } } } </script> <style lang="less" scoped> .top_tips { position: absolute; width: 3.25rem; height: 7.35rem; top: -1.3rem; right: 1.6rem; background: url('@/assets/images/WechatIMG2.png') no-repeat; background-size: 100% 100%; .num_tip { position: absolute; width: 2.5rem; height: 0.7rem; left: 0.48rem; bottom: 1.1rem; font-size: 0.6rem; font-family: '黑体'; font-weight: 600; color: #a57c50; text-align: center; } } .item_container_style { position: absolute; width: 13.15rem; height: 11.625rem; top: 4.1rem; left: 1rem; } .next_item { background-image: url(@/assets/images/2-2.png); } .submit_item { background-image: url(@/assets/images/3-1.png); } .home_logo { background: url('@/assets/images/1-2.png') no-repeat; background-size: 100% 100%; } .button_style { display: block; width: 4.35rem; height: 2.1rem; position: absolute; top: 16.5rem; left: 50%; margin-left: -2.175rem; background-size: 100% 100%; background-repeat: no-repeat; } .start { background-image: url('@/assets/images/1-4.png'); } .item_back { background-image: url('@/assets/images/2-1.png'); background-size: 100% 100%; .item_list_container { position: absolute; width: 8rem; height: 7rem; top: 2.4rem; left: 3rem; .item_title { font-size: 0.65rem; color: #fff; line-height: 0.7rem; } .item_list { width: 10rem; margin-top: 0.4rem; span { display: inline-block; font-size: 0.6rem; color: #fff; text-align: center; line-height: 0.725rem; margin-left: 0.3rem; } .option_style { width: 0.725rem; height: 0.725rem; border: 1px solid #fff; border-radius: 50%; } .has_choosed { background-color: #ffd400; color: #575757; border-color: #ffd400; } } } } </style>
5. 组件引入
home/index.vue:
<template> <div class="home_container"> <Item father-component="home"></Item> </div> </template> <script> import Item from '@/components/item.vue' export default { components:{ Item } } </script> <style lang="less" scoped></style>
item/index.vue:
<template> <div> <Item father-component="item"></Item> </div> </template> <script> import Item from '@/components/item.vue' export default { components:{ Item } } </script> <style lang="less" scoped> </style>
6. 路由配置(router/index.js)
import { createRouter, createWebHistory } from 'vue-router' import Home from '@/views/home' const routes = [ { path:'/', redirect:'/home' //重定向 }, { path:'/home', //根路径下展示 name:'home', component:Home }, { path:'/item', name:'item', component:()=>import('@/views/item') }, { path:'/score', name:'score', component:()=>import('@/views/score') } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router
7. 得分页(score/index.vue)
思路:
- 修改图片背景。
- 得到用户选中答案的result数组后,利用forEach遍历找到和后端数据匹配题目的选项并判断选中的选项是否为正确答案,是则加上这一题的分数。
- 利用得到的总分计算答对的题数,把它作为提示语数组的下标,这样不同的分数就能对应不同的提示语了。
<template> <div class="score_container"> <header class="your_score"> <span class="score_num">{{ score }}分</span> <span class="res_tip">{{ getScoreTip() }}</span> </header> </div> </template> <script> import { questions } from '@/mock' import { useRoute } from 'vue-router' export default { setup() { const route = useRoute() console.log(route.query.answer); //修改body的背景 const bg = require('@/assets/images/4-1.jpg') document.body.style.backgroundImage = `url(${bg})` let score = 0 //计算得分 function calcScore(id, idx) { //id为选中的选项,idx为第几题 questions[idx].topic_answer.forEach((answerItem) => { if (answerItem.topic_answer_id == id && answerItem.is_standard_answer === 1) { score += (100 / questions.length) } }) } route.query.answer.forEach((id, index) => { calcScore(id, index) }) const scoreTipsArr = [ "你说,是不是把知识都还给小学老师了?", "还不错,但还需要继续加油哦!", "不要嘚瑟还有进步的空间!", "智商离爆表只差一步了!", "你也太聪明啦!", ] const getScoreTip = () => { let every=100/questions.length let index=Math.ceil(score/every)-1 return scoreTipsArr[index] } return { score,getScoreTip } } } </script> <style lang="less"> #app { overflow: hidden; } .score_container { width: 9.7rem; height: 9.1rem; background-image: url('@/assets/images/4-2.png'); background-repeat: no-repeat; background-size: 100% 100%; margin: 0 auto; margin-top: 1rem; position: relative; .your_score { position: absolute; right: 0; width: 9rem; text-align: center; font-size: 1.4rem; top: 4.7rem; font-weight: 900; -webkit-text-stroke: 0.05rem #412318; .score_num { color: #a51d31 } .res_tip { display: block; color: #3e2415; font-size: 0.7rem; font-weight: 200; margin-top: 1rem; } } } </style>
最后
到此这篇关于vue3实现问卷调查的示例代码的文章就介绍到这了,更多相关vue3 问卷调查内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!