蓝桥杯Web第十四届蓝桥杯Web模拟赛 3 期 | 精品题解(上)

Posted 海底烧烤店ai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了蓝桥杯Web第十四届蓝桥杯Web模拟赛 3 期 | 精品题解(上)相关的知识,希望对你有一定的参考价值。


🧑‍💼 个人简介:一个不甘平庸的平凡人🍬
🖥️ 蓝桥杯专栏:蓝桥杯题解/感悟
🖥️ TS知识总结:十万字TS知识点总结
👉 你的一键三连是我更新的最大动力❤️!
📢 欢迎私信博主加入前端交流群🌹


📑 目录


🔽 前言

好久没更新了,今天上线收到了很多小伙伴的私信,发现有很多朋友正在准备蓝桥杯,问我要不要出几天前开放的模拟赛 3 期的解析,其实这一期的模拟赛刚开始时我就知道了,但根据以往CSDN平台的限制以及我确实没想到会有这么多人想看,于是就没第一时间出。

看到了大家的催更后,我想着就发一下试试吧,希望能顺利发出,话不多说,开干!

由于篇幅有限,这次题解将分为上下两篇,这一篇先讲本科组的前八题,下一篇中我们再讲本科组的最后两题和职业院校组中和本科组不一样的题。

1️⃣ 网页PPT

使用简单的JQuery就能实习:

function switchPage() 
  // TODO: 请补充该函数,实现根据activeIndex切换页面的功能,并且在到达最后一页或第一页时给相应的按钮添加disable类
  // 1. 设置指定页面显示
  $("section").each((index, item) => 
    $(item).css("display", index !== activeIndex ? "none" : "block");
  );
  // 2. 修改页码
  $(".controls .page").text(`$activeIndex + 1 / 5`);
  // 3. 根据情况修改按钮的class
  if (activeIndex === 0) 
    $(".btn.left").addClass("disable");
   else if (activeIndex === sectionsCount - 1) 
    $(".btn.right").addClass("disable");
   else 
    $(".btn.left").removeClass("disable");
    $(".btn.right").removeClass("disable");
  

2️⃣ 西游记之西天取经

考察CSS3动画属性,题目中说动画只动一次就会停下来,这明显是没有设置动画的animation-iteration-count(动画执行次数)导致的,所以想要让动画无限循环起来,只需要为每个animation复合属性添加infinite(无限循环)即可:

/* TODO 填空 */
animation: a3 0.8s steps(8) infinite ; 

3️⃣ 商品销量和销售额实时展示看板

考查echarts的基础使用,挺简单的,直接上代码:

// TODO:补全 `yAxis` 的设置,要求“销售额”(即,配置项 `name`)的位置(即,配置项 `position`)在图表的左侧,“销量”(即,配置项 `name`)的位置(即,配置项 `position`)在图表的右侧。
yAxis: [
    type: 'value',
    name: '销售额',
    position: 'left',
,

    type: 'value',
    name: '销量',
    position: 'right',
],

// TODO:补全代码,正确给 X 轴的时间,以及 Y 轴的商品的销售额 saleObj 和销量赋值 countObj。
charData.xAxis.data = Object.keys(result.data.countObj); // 获得由全部key组成的数组
charData.series[0].data = Object.values(result.data.saleObj); // 获得由全部value组成的数组
charData.series[1].data = Object.values(result.data.countObj);

4️⃣ 蓝桥校园一卡通

考察一些简单的正则与表单验证,这里我封装了一个validate函数专门用来验证输入与控制DOM

submit.onclick = () => 
  // TODO 待补充代码
  /**
   * 用于验证输入并控制相关DOM操作
   * @param RegExp reg 用于检验文本的正则
   * @param Element inputNode input元素
   * @param String parentNodeClass 需要给父节点添加的类名
   * @param String errorNodeId 错误信息节点的id
   * @return *
   */
  function validate(reg, inputNode, parentClass, errorId) 
    const parentElement = inputNode.parentElement // 获取父元素
    const errorElement = parentElement.querySelector('#' + errorId) // 获取错误信息元素
    let result = false // 是否通过验证
    if (!reg.test(inputNode.value)) 
      // 输入不匹配
      parentElement.classList.add(parentClass) // 给父节点添加class
      errorElement.style.display = 'block' // 显示错误信息
     else 
      // 输入匹配
      parentElement.classList.remove(parentClass) // 移除父节点class
      errorElement.style.display = 'none' // 隐藏错误信息
      result = true
    
    return result
  
  // 姓名
  let regName = /^[\\u4e00-\\u9fa5]2,4$/g
  const nameVail = validate(regName, studentName, 'has-error', 'vail_name')
  if (!nameVail) 
    return
  
  // 学号
  let regStuId = /^\\d1,12$/g
  const stuIdVail = validate(regStuId, studentId, 'has-error', 'vail_studentId')
  if (!stuIdVail) 
    return
  

  item[0].innerhtml = studentName.value
  item[1].innerHTML = studentId.value
  item[2].innerHTML = college.value

  // 添加 showCard 类显示放大一卡通的动画,请勿删除
  cardStyle.classList.add('showCard')

5️⃣ 会员权益领取中心

每次蓝桥杯的题中都会有一道这种仿页面的题,没啥技术含量,就是纯体力活,这里就不贴代码了。大家在考试的时候切记不要浪费太多时间在这道题上,很容易吃力不讨好。

6️⃣ 心愿便利贴

这题主要考察了element-ui表单验证,根据代码中的提示发现有两处需要修改。

一是,题中给出的代码中v-for遍历的是一个空数组v-for="(item,index) in []" ,我们需要找到存放数据的数据来替换这个空数组,翻到下面的提交方法onSubmit中很容易发现数据存放在wishList中,所以:

<!-- TODO 待修改的代码 -->
<div class="card" :class="item.css" v-for="(item,index) in wishList" ...

二是,需要补充rules中的验证规则:

rules: 
	// TODO 待补充验证的代码
	name: [
		 required: true, message: '请输入姓名', trigger: 'blur' ,
		 min: 2, max: 4, message: '长度在 2 到 4 个字符', trigger: 'blur' 
	],
	content: [
		 required: true, message: '请输入许愿内容', trigger: 'blur' ,
		 min: 1, max: 30, message: '长度在 1 到 30 个字符', trigger: 'blur' 
	],
,

每条规则的key对应的是el-form-item上的prop属性,根据题目的示例,这两天规则很容易写出,这里就不多说了。

7️⃣ 消失的 Token

考察Vuex的使用,题目中明确说了:“仔细阅读 store 文件夹下的相关代码并结合 Vuex 相关知识,排查代码中存在的问题”。

所以我们应该先去看一下Vuex相关的代码,在UserModule.js中会发现UserModule这个模块开启了命名空间:

UserModule.js

const UserModule = 
    namespaced: true, // 开启了命名空间
    ...

index.js中是通过user字段来引入UserModule的:
index.js

const store = new Vuex.Store(
    modules: 
        base: BaseModule,
        user: UserModule, // 注意
    ,
)

所以在使用UserModule模块的内容时就需要注意要通过命名空间user来引用,之后检查index.html就很容易发现问题所在了:

// TODO 修改下面错误代码
var app = new Vue(
  el: '#app',
  data() ,
  computed: 
    welcome() 
      return store.getters.welcome
    ,
    username() 
      return store.getters['user/username'] // 修改处:通过命名空间引用
    ,
    token() 
      return store.getters['user/token'] // 修改处:通过命名空间引用
    
  ,
  methods: 
    // 回车/点击确认的回调事件
    login(username) 
      // 修改处:通过命名空间引用
      username && store.commit('user/login',  username, token: 'sxgWKnLADfS8hUxbiMWyb' )
      username && store.commit('say', '登录成功,欢迎你回来!')
    
  
)

8️⃣ 封装 Promisefy 函数

考查了promise的使用,在题中我们很容易发现promisefy有以下几个需求:

  1. 需要返回一个函数,并且返回的这个函数还需要返回一个promise
  2. promisefy接收的fn参数是个函数,它最后一个参数是个回调函数,回调函数的第一个参数代表 err 信息,第二个参数代表成功返回的结果。

代码:

const promisefy = (fn) => 
  // TODO 此处完成该函数的封装
  return (...arg) =>  // 使用剩余参数arg收集所传递的实参
    return new Promise((resolve, reject) => 
      // 将arg解构传递给fn
      fn(...arg, (err, contrast) => 
        if (err) 
          // err存在代表fn运行出错了,调用reject将promise的状态转换为rejected并传递err,此时该promise的catch方法将捕捉到该err错误
          reject(err)
        
        // 如果代码运行到这说明没出错,调用resolve将promise的状态转换为fulfilled并传递contrast,此时该promise的then方法将捕捉到该contrast信息
        resolve(contrast)
      )
    )
  

上面代码也挺简单的,需要注意的一点就是promise的状态一旦改变就不能再次改变了,所以上面代码中如果先调用了reject,则后面的resolve调用就不再起作用了,所以在if语句中不需要加return来阻断后续代码的执行。

🔼 结语

距离第十四届蓝桥杯的正式比赛还有不到一个月的时间,好好复习,祝大家都能在正式比赛中取得满意的成绩!

如果本篇文章对你有所帮助,还请客官一件四连!❤️

蓝桥杯Web第十四届蓝桥杯Web模拟赛 3 期 | 精品题解(下)


🧑‍💼 个人简介:一个不甘平庸的平凡人🍬
🖥️ 蓝桥杯专栏:蓝桥杯题解/感悟
🖥️ TS知识总结:十万字TS知识点总结
👉 你的一键三连是我更新的最大动力❤️!
📢 欢迎私信博主加入前端交流群🌹


📑 目录


🔽 前言

昨天更新了第十四届蓝桥杯Web模拟赛 3 期的一些基础题的解析,今天抽时间把剩余的压轴题的解析肝出来了,本科组最后的两个题加上职业院校组中与本科组不同的一个题,总共三题,这三题还是有一定难度的,各位小伙伴们加油!

9️⃣ 趣购

这一题挺有趣的,考的拖放Api在平时开发中不是很常见,但考的Vue计算属性还是挺有用的,我们先从事件下手:

<div class="good-list">
  <div v-for="good in goods" 
  :key="good.name" 
  class="good" 
  draggable="true" 
  @dragstart="dragstart($event,good)">
    <img :src="good.cover" />
    <span> good.name </span>
    <span> good.price </span>
  </div>
</div>

上面先为每个商品绑定draggable="true"使其变成可拖放元素,再为其绑定dragstart事件,其对应的dragstart事件处理程序如下:

dragstart(ev,good)
  	// 向dataTransfer属性中添加拖拽数据
    ev.dataTransfer.setData("name", good.name);
    ev.dataTransfer.setData("price", good.price);

根据题目信息,我们很容易知道可以在dataTransfer 属性中保存事件的数据。

draggable:这个属性是枚举类型 (en-US),而不是布尔类型。这意味着必须显式指定值为 true 或者 false,像 <img draggable> 这样的简写是不允许的。正确的用法是 <img draggable="false">
dragstart事件:当用户开始拖拽一个元素或选中的文本时触发

之后需要为购物车图标绑定放置事件drop

<div id="trolley" class="trolley" @dragover.prevent @drop="drop" >
  <span id="bought" class="bought" v-if="bought.length !== 0">
    bought.length
  </span>
  <img src="./images/trolley.jpeg" />
</div>

根据题目信息可以得知,可以通过drop事件来获取可拖放元素的数据,而要想触发drop事件需要先清除dragover事件的默认行为,在Vue中可以通过.prevent修饰符来清除事件的默认行为,所以在购物车图标上还需要绑定一个@dragover.preventdrop事件对应的事件处理程序如下:

drop(ev)
  // 先获取dataTransfer上保存的可拖放元素的数据
  const name = ev.dataTransfer.getData("name");
  const price = ev.dataTransfer.getData("price");
  // 向bought数组中添加该商品的信息(向购物车中添加商品)
  this.bought.push(name,price:Number(price)) // 这里将price转换成number类型,方便之后的计算
,

观察题目代码很容易推断出data中的bought是用来存放购物车的数据的

通过上面的步骤后题目的要求我们已经实现了一半了,下面需要解决的问题就是将购物车(bought)内的数据渲染到页面上,观察发现页面中使用到了两个计算属性来显示购物车(bought)数据:

<div class="result">
  <div>
    购物车商品:<span id="goods"> goodsDetail </span>
  </div>
  <div>
    购物车商品总计:<span id="total"> totalPrice </span>
  </div>
</div>

所以接下来我们只需要补全goodsDetailtotalPrice这两个计算属性就ok了:

totalPrice() 
  // 通过数组的reduce求和函数来获取购物车商品总计
  return this.bought.reduce((a, b) => 
    return a + b.price
  , 0);
,
goodsDetail() 
  /**
   * 这里用了两次reduce
   * 第一次是为了将bought中相同的商品合并为同一个对象,并为其添加一个amount字段表示其数量
   * 第二次是为了将数据转换成符合题目要求的字符串格式
   */
  return this.bought.reduce((a, b) => 
    const good = a.find(item => item.name === b.name) // 先查询a中与b相同的商品
    if (good) 
      // 如果a中有与b相同的商品,则将其amount加1即可
      good.amount++
     else 
      // 如果a中没有与b相同的商品,则向其push b商品的信息并初始化一个amount字段
      a.push( name: b.name, price: Number(b.price), amount: 1 )
    
    return a
  , []).reduce((a, b) => 
    return a + b.name + '*' + b.amount + ' '
  , '');
,

上面代码中使用了数组的reduce方法,对该方法不熟悉的小伙伴可查阅:MDN reduce

🔟 分页组件

到了本科组压轴的题了,这一题的要求还是挺多的,任务量比较大,但好在题中是根据任务数来给分的,所以遇到这种题不要慌,一步一步的向下走就好。

目标一:

/**
 * @description ajax 请求,通过传递的 currentPage, pageSize 获取到当前页和总页数的数据
 * @param string url 请求地址,必填
 * @param string method 请求方式,可选参数,默认为 get
 * @param string data 请求体数据,可选参数
 * @param number currentPage 当前页数,必填
 * @param number pageSize 每页显示条目个数,必填
 * @return object data,total data为data.json中data数组的部分数据,total为data.json中total的值
 * */
async function ajax(
  url,
  method = "get",
  data,
  query:  currentPage, pageSize ,
) 
  // TODO:根据函数参数 `query` 对象  `currentPage, pageSize` 获得当前页的数据
  let result = 
    data: [],
    total: 0,
  ;

  let res = await axios[method](url, data); // 获取请求结果
  let resData = res.data.data;
  result.total = resData.length;
  result.data = resData.splice((currentPage - 1) * pageSize, pageSize); // 通过splice方法将当前页的数据截取出来

  return result;

目标二:

/**
 * @description 事件绑定,改变 this.currentPage 的值,值在 1 到 this.totalPages 之间
 **/
initEvents() 
  this.root.querySelector("#btn-prev").addEventListener("click", () => 
    // TODO:"<" 按钮的点击事件, 点击时 this.currentPage - 1
    if (this.currentPage > 1) 
      this.currentPage--;
      this.initPagination();
    
  );
  this.root.querySelector("#btn-next").addEventListener("click", () => 
    // TODO:">" 按钮的点击事件, 点击时 this.currentPage + 1
    if (this.currentPage < this.totalPages) 
      this.currentPage++;
      this.initPagination();
    
  );
  this.root.querySelector(".pager").addEventListener("click", (e) => 
    if (e.target.nodeName.toLowerCase() === "li") 
      if (this.currentPage === e.target.innerText) return;
      if (e.target.classList.contains("more")) return;
      this.currentPage = Number(e.target.innerText);
    
    this.initPagination();
  );

补全initEvents函数并不能,根据事件控制currentPage的值即可,需要注意的就是每当currentPage的值改变都要调用一下initPagination事件(这一点题目代码中initPagination事件上的注释里明确说到了)。

initEvents函数补全之后提交测试我们还不能通过目标二,这是因为负责渲染分页按钮的renderPagination函数还没有补全(目标四的要求),导致页面上还不能正确显示分页组件,所以系统才会不让通过,目标三提交不通过也是这个原因。

也就是说不先通过目标四,目标二和目标三完成了也不会通过,那这题分目标给分的意义在哪呢🙄)

目标三:

个人认为目标三是最为复杂的一个任务,主要是你要能想到这种逻辑,下面代码中我分了两大类情况:

  1. totalPages<=pagerCount时直接遍历totalPages向数组中添加页码就行,这没什么好说的。
  2. totalPages>pagerCount时就比较复杂了,需要再考虑三种情况,也就是题目中给的例子[1,2,3,4,10],[1,3,4,5,10],[1,7,8,9,10]这三种情况,观察这三个数组很容易发现:
    • [1,2,3,4,10]是靠左显示,向右扩散的;也就说从左向右读是连续的,从右向左读会出现断层(4和10)
    • [1,3,4,5,10]是中间显示,向两边扩散;也就说不管是从左向右读还是从右向左读都会出现断层(1和3,5和10)
    • [1,7,8,9,10]是靠右显示,向左扩散的;也就说从右向左读是连续的,从左向右读会出现断层(1和7)

上面关于第二类的三种情况的说明可能不是很准确,大家明白这个意思就行。

了解了情况后,直接看代码:

/**
 * @description 得到分页数组 indexArr,如[1,2,3,4,10],[1,3,4,5,10],[1,7,8,9,10]
 * @param number currentPage 当前页数,默认为第一页
 * @param number totalPages 总的页码数
 * @param number pagerCount 设置最大页码按钮数。 页码按钮的数量,当总页数超过该值时会折叠
 * @return Array 分页数组 indexArr
 */

const createPaginationIndexArr = (currentPage, totalPages, pagerCount) => 
  let indexArr = [];
  // TODO:根据传参生成分页数组 indexArr
  indexArr[0] = 1; // 第一项肯定是1
  
  if (!currentPage) 
    // currentPage不存在时默认为1
    currentPage = 1;
  
  if (!totalPages) 
    // totalPages不存在时默认为currentPage
    totalPages = currentPage;
  
  if (!pagerCount) 
    // pagerCount不存在时默认为5
    pagerCount = 5;
  
  // 上面三个判断可以不要

  let medial = Math.floor(pagerCount / 2); // 中间位置

  if (totalPages <= pagerCount) 
    for (let i = 1; i < totalPages; i++) 
      indexArr[i] = i + 1;
    
   else 
    indexArr[pagerCount - 1] = totalPages; // 最后一项为totalPages
    // 当前页数靠左边,则从左向右扩散添加
    // 例如当前页数是2,3,4,总页数为10,页码按钮数是5时:[1,2,3,4,10]
    if (currentPage <= medial && totalPages - currentPage > medial) 
      for (let i = 1; i < pagerCount - 1; i++) 
        indexArr[i] = i + 1;
      
    
    // 当前页数在中间,则从中间向两边扩散
    // 例如当前页数是4,总页数为10,页面按钮数是5时:[1,3,4,5,10]
    if (currentPage > medial && totalPages - currentPage > medial) 
      indexArr[medial] = currentPage; // 中间位置设置为当夜页数
      for (let i = medial - 1, c = 1; i > 0; i--, c++) 
        indexArr[i] = currentPage - c; // 向左扩散添加
        if (medial + c < pagerCount - 1) 
          indexArr[medial + c] = currentPage + c; // 向右扩散添加
        
      
    
    // 当前页数在右边,则从右向左扩散添加
    // 例如当前页数是7,8,9,总页数为10,页面按钮数是5时:[1,7,8,9,10]
    if (currentPage > medial && totalPages - currentPage <= medial) 
      for (let i = pagerCount - 2, c = 1; i > 0; i--, c++) 
        indexArr[i] = totalPages - c;
      
    
  

  return indexArr;
;

module.exports = 
  createPaginationIndexArr,
;

在第二类情况下indexArr数组的第一项一定是1,最后一项一定是totalPages,所以我们只需要再根据那三种情况填充indexArr中剩余位置的空间即可。

目标四:

/**
 * @description 根据序号数组生成分页组件的字符串模板通过 innerHTML 挂载在 root 元素内
 * @param Array indexArr 分页数组 indexArr
 * @return String 分页组件的字符串模板
 */
renderPagination(indexArr) 
  let template = "";
  // TODO:根据 indexArr 数组生成分页组件的字符串模板 template
  template = indexArr
    .map((item, index) => 
      let more = `<li class="number more">...</li>`;
      let str = `<li class="number $item === this.currentPage ? "active" : """>$item</li>`;

      if (index > 0 && item - indexArr[index - 1] > 1) 
        // 如果当前item与上一个item(index[index-1])的差值大于1,则需要在当前分页按钮的前面添加...
        return more + str;
      
      return str;
    )
    .join("");

  this.root.innerHTML = `
      <div class="pagination">
          <div class="btn btn-left" id="btn-prev">&lt;</div>
          <ul class="pager">$template </ul>
          <div class="btn btn-right" id="btn-next">&gt;</div>
      </div>`;

🔷 虚拟滚动列表(职业院校组)

这一题考察日常开发中常见的列表优化方式:虚拟滚动列表。

先通过axios获得全部的数据:

mounted() 
  // TODO: 完成数据请求
  axios.get("./data.json").then((res) => 
    this.list = res.data; // 存放总数据
    this.totalHeight = res.data.length * this.itemHeight; // 总高度
  );
,

然后对容器绑定滚动事件scroll

  <div id="virtual-list" class="virtual-list" @scroll="scroll">
methods: 
  // TODO: 完成事件处理
  scroll(e) 
    this.start = Math.floor(e.target.scrollTop / this.itemHeight);
  ,
,

scroll事件中,我们通过滚动条已经滚动的高度/每一项的高度来获取可视区域内的第一项的下标(this.start)。

例如:已经滚动300,每一项高度为100,则已经滚动了三项了,目前可视区域内的第一项应为第四项,其对应的下标就为3

计算出需要渲染到页面上的列表:

computed: 
  showingList() 
    let sliceStart = this.start > this.buffer ? this.start - this.buffer : 0;
    let sliceEnd = this.start + this.length + this.buffer + 1;
    return this.list.slice(sliceStart, sliceEnd);
  ,
,

题目中给了buffer这个字段,其目的是为了防止出现白屏,在我们每次计算需要渲染到页面上的列表时向前面多计算buffer个,向后面也多计算buffer个。

showingList方法中我们主要通过数组的slice方法来截取到需要渲染到页面上的部分,其中:sliceStart表示开始截取的下标,sliceEnd表示结束截取的下标(因为是截取不到sliceEnd位置的,所以需要提前+1)。

this.start改变时,showingList计算属性会重新执行,于是就能获取到每次滚动时需要渲染到页面中的数据。

只是获取到数据还不行,当我们进行滚动时需要将列表项容器也进行位移,这样才能保证数据一直在可视区域内:

<ul
  id="list"
  class="list"
  :style="
    transform:
      'translateY(' +
      (start > buffer ? (start - buffer) * itemHeight : 0) +
      'px)',
  ">

start小于buffer时,由于列表可视区域下方还有buffer个元素,当我们滚动时列表项能自然进行滚动,所以不需要设置translateY,只有当start>buffer时才需要设置translateY

实现虚拟列表的代码量并不多,主要还是在于逻辑。

🔼 结语

距离第十四届蓝桥杯的正式比赛还有不到一个月的时间,好好复习,祝大家都能在正式比赛中取得满意的成绩!

如果本篇文章对你有所帮助,还请客官一件四连!❤️

以上是关于蓝桥杯Web第十四届蓝桥杯Web模拟赛 3 期 | 精品题解(上)的主要内容,如果未能解决你的问题,请参考以下文章

蓝桥杯Web第十四届蓝桥杯(Web 应用开发)模拟赛 1 期-大学组 | 精品题解

蓝桥杯Web第十四届蓝桥杯(Web 应用开发)模拟赛 1 期-职业院校组 | 精品题解

第十四届蓝桥杯(Web应用开发)模拟赛1期-大学组

第十四届蓝桥杯(web应用开发)模拟赛2期 -大学组

蓝桥杯Web第十四届蓝桥杯(Web 应用开发)模拟赛 2 | 精品题解

第十四届蓝桥杯python第一期模拟赛