likes
comments
collection
share

vue3实现一个【问卷调查】

作者站长头像
站长
· 阅读数 19

用vue3制作【问卷调查】

前言

问卷调查,这个东西真的随处可见,那不如自己做一个问卷调查?话不多说,我们来实现它!!! 我们需要实现的效果图如下:

vue3实现一个【问卷调查】

开发工具

vscode(里面预先装好vue)

思路准备

通过分析调查问卷的功能,我们来梳理一下实现它的方式:

  1. 首先我们把这个问卷调查分为三个板块首页(home),答题页(item),得分页(score)。实现这三个板块的跳转采用路由的方式。
  2. 我们可以看到 首页(home) 和 答题页(item) 中每道题目的切换都只是中间部分发生变化,其他的地方并没有改变,那么我们可以把其作为一个组件分离出去,在这个组件中完成代码编写。
  3. 最后单独编写得分页(score)。

正文

一、创建vue3项目

我们使用vue create xxx命令创建这个项目,我这以happy命名,并安装好路由less预处理器

二、构建目录结构

vue3实现一个【问卷调查】

创建完毕后,我们对这些文件夹做进一步操作:
  • 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>

最后

以上就是调查问卷的详细实现过程,如果觉得还不错的话,记得点个赞支持呀! 文章可能有一些错误,欢迎评论指出,也欢迎一起讨论。