?? 个人简介:一个不甘平庸的平凡人?
?️ 蓝桥杯专栏:蓝桥杯题解/感悟
?️ TS知识总结:十万字TS知识点总结
? 你的一键三连是我更新的最大动力❤️!
? 欢迎私信博主加入前端交流群?
? 目录
? 前言1️⃣ 凭空消失的 TA2️⃣ 用户名片3️⃣ 芝麻开门4️⃣ 宝贵的一票5️⃣ 粒粒皆辛苦6️⃣ 618 活动7️⃣ 资讯接口8️⃣ 绝美宋词9️⃣ 平地起高楼? 收快递了? 偷梁换柱(职业院校组)? 大电影(职业院校组)? 乾坤大挪移心法(职业院校组)? 不能说的秘密(职业院校组)? 结语
? 前言
第十四届蓝桥杯Web应用开发模拟赛第二期昨天正式开始了(本来写的是今天正式开始了,结果没想到这篇文章写到了凌晨1点??),博主也是第一时间为大家带来了题解!这篇题解包含了大学组和职业院校组的所有内容。
因为自己在做题时忘记保存代码了,所以写这篇题解时我不得不又重新做了一遍,看在博主这么肝的份上,大佬们给个一键三连加关注吧!?
关于蓝桥杯更多的题解请前往专栏:蓝桥杯题解/感悟,欢迎大家的订阅!
本篇只会大概提出题目要求,关于题目的更多细节可自行去模拟赛主页查询:Web 应用开发模拟赛 2 期大学组
话不多说,开撕!
1️⃣ 凭空消失的 TA
题目说在 index.html
中未正常显示表单组件 myform
,先运行看一下效果:
发现 myform
组件里的立即创建
和取消
这两个文本被渲染了,这说明 index.html
确实是引入了 myform
,但为何myform
没有正常显示呢?
一开始我以为是myform
组件里出了问题,可检查一遍后并没有发现问题,最后回到index.html
才发现,是因为index.html
中未引入element-ui
的js
文件,我们加一行代码引入一下就解决了:
<!-- 引入 element-ui 样式 --> <link rel="stylesheet" href="./element-ui-2.15.10/index.css" /> <!-- 新增:引入 element-ui js文件 --> <script src="./element-ui-2.15.10/index.js"></script>
2️⃣ 用户名片
要求是需要将这个卡片垂直居中,并且还需要将卡片中左侧文字水平居中,看了一下HTML结构,发现它们都有一个共同的类名center
:
所以对center
类名定义样式就行了:
/* TODO 待补充代码 */.center { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);}
上述代码利用定位将元素垂直水平方向各偏移父元素(position: relative
)的50%,这个时候元素还不是居中(因为定位偏移的中心点不在元素的中心上而是在元素的边界上):
使用transform
将元素在水平和垂直的负方向移动自身的50%(transform
运动的中心点在元素的中心位置):
这里深入说一下一个CSS选择器优先级的问题:
图中所示,作用于同一元素的.user-card .points
(后代选择器)的样式优先生效于.center
(类选择器)的样式,这就映证了网上说的后代选择器的优先级小于类选择器的说法是不够准确的。
其实,后代选择器和类选择器没有可比性,后代选择器是选择器组合方式的一种,它是一种组合,本身没有任何优先级(严格的用词叫特殊性) 可言。比如.user-card .points
在计算特殊性(优先级)时,是分别计算「.user-card」和「.points」的特殊性(优先级),完全不用考虑它们之间是用后代关联的。
按照权重来说.user-card .points
的样式优先生效于.center
的样式,是因为.user-card .points
含有两个类选择器,它的权重比.center
高。
关于CSS选择器权重、优先级的问题在实际开发中是比较重要的,如果你看到这里对它们还不是很了解,建议你去网上多看看关于它们的内容。
3️⃣ 芝麻开门
这题简单的考察了Promise
,最终实现以下效果:
代码:
/** * @description: 调用函数,开启弹窗,记录输入框的内容,并通过 promise 异步返回输入框中的内容 * @return {Promise} */function mPrompt() { // 弹窗必须使用以下结构 template 保存的是弹窗的结构字符串,可以先转化为 DOM 再通过 appendChild 方式插入到 body 中 const template = ` <div class="modal"> <div class="message-box"> <div class="message-header">请输入咒语</div> <div class="message-body"> <input type="text"> </div> <div class="message-footer"> <button class="btn btn-small" id='cancel'>取消</button> <button class="btn btn-small btn-primary" id='confirm'>确定</button> </div> </div> </div> `; const div = document.createElement("div"); // TODO:待补充代码 div.innerHTML=template document.body.append(div); let val = div.getElementsByTagName("input")[0]; return new Promise((resolve, reject) => { document.getElementById("cancel").onclick = function() { div.remove() reject(false) } document.getElementById("confirm").onclick = function() { div.remove() resolve(val.value) } });}
代码很简单,按照题目要求返回一个Promise
对象,并在点击事件中做出不同的处理(reject
,resolve
)即可。
4️⃣ 宝贵的一票
要求是实现一个动态列表的表单,可以新增选项和删除选项,最终效果:
添加的思路:
删除的思路:
点击删除号(x)时先删除当前选项。遍历余下的选项列表,更新它们的序号。在遍历的时候判断余下的选项个数,若剩余的选项小于等于2了,需要删除每个选项后面的删除号(x)。代码:
// 点击加号逻辑$(".add").click(function () { // TODO 待补充代码 // 当前列表长度 let cl = $(".list").children().length; // 长度为2时为前两个选项加上x号 if (cl === 2) { $(".list").children().each((index,item)=>{ $(item).append(` <div class="col-sm-1"> <img class="del-icon" src="./images/x.svg" alt="" /> </div>`) }) } if (cl < 2) { // 当前列表长度小于2时,添加不带x号的选项 $(".list").append(initRender(`选项${cl + 1}`)); }else { // 当前列表长度大于等于2时,添加带x号的选项 $(".list").append(`<div class="mb-3 row item"> <label class="col-sm-2 col-form-label txt">选项${cl + 1}</label> <div class="col-sm-9"> <input type="text" class="form-control" /> </div> <div class="col-sm-1"> <!-- 删除图标 --> <img class="del-icon" src="./images/x.svg" alt="" /> </div> </div>`); }});// 点击 x 删除逻辑,列表小于 2 项时不显示删除图标$(document).on("click", ".del-icon", function () { // TODO 待补充代码 // 删除这一条 $(this).parent().parent().remove() // 遍历 $(".list").children().each((index,item)=>{ // 修改剩下的列表序号 $(item).children('label').text(`选项${index + 1}`) if($(".list").children().length <= 2) { // 列表长度小于等于2时,请求x号 $(item).children()[2].remove() } })});
5️⃣ 粒粒皆辛苦
这是一道ECharts
题,从历届蓝桥杯Web比赛、模拟赛等可以看出每一次比赛都至少会有一道ECharts的题,不过这些ECharts
题涉及到的ECharts
的内部并不过,大部分都只是考察你对数据的处理,比如这一题,本质就是对数据格式的转换。
源数据格式:
{ "2017": { "wheat": 431, "soybean": 142, "potato": 232, "corn": 642 }, "2018": { "wheat": 417, "soybean": 156, "potato": 258, "corn": 643 }, "2019": { "wheat": 416, "soybean": 168, "potato": 269, "corn": 650 }, "2020": { "wheat": 436, "soybean": 174, "potato": 277, "corn": 680 }, "2021": { "wheat": 441, "soybean": 186, "potato": 289, "corn": 692 }, "2022": { "wheat": 445, "soybean": 201, "potato": 315, "corn": 706 }}
字段对应表:
英文名称 | 中文名称 |
---|---|
wheat | 小麦 |
soybean | 大豆 |
potato | 马铃薯 |
corn | 玉米 |
需要转换成的数据格式:
[['全部', '2017', '2018', '2019', '2020', '2021', '2022'], ['小麦', 431, 417, 416, 436, 441, 445], ['大豆', 142, 156, 168, 174, 186, 201],['马铃薯', 232, 258, 269, 277, 289, 315],['玉米', 642, 643, 650, 680, 692, 706]]
代码:
// TODO: 待补充代码let dataObj = { wheat: ["小麦"], soybean: ["大豆"], potato: ["马铃薯"], corn: ["玉米"]};let sourceTip = ["全部"];// 获取数据axios.get("./data.json").then(res=>{ let data = res.data.data; for (const key1 in data) { sourceTip.push(key1); for (const key2 in data[key1]) { dataObj[key2].push(data[key1][key2]); } } let newSource = []; newSource.push(sourceTip); for (const key in dataObj) { newSource.push(dataObj[key]); } option.dataset.source = newSource; myChart.setOption(option);})
代码和逻辑都比较简单,就不多说了。
6️⃣ 618 活动
就是按照官方给的最终效果图,去实现下面这个页面:
没啥技术含量,全靠堆HTML和CSS,这里就不放代码了。
但这个题是我认为是整场模拟赛里最坑人的题,特别废时间,我建议这个题要么放到最后再写(因为完成度50%以上就能得到分,其它题不行),要么完成差不多后就直接去做下面的题,别死扣细节,不然吃亏的都是你!
7️⃣ 资讯接口
题目要求使用 NodeJS
去创建一个服务器并响应一个/news
接口:
app.js
书写代码,创建一个服务器,使服务在 8080 端口运行。访问 /news
返回资讯数据,访问其他任意路径均返回字符串 404 。 代码:
// TODO: 待补充代码const http = require("http");// 创建http服务const app = http.createServer();app.on("request",(req,res)=>{ res.setHeader("Content-type", "text/html;charset=utf8"); switch (req.url) { case '/news': res.end(JSON.stringify([ { "channelId": "5572a108b3cdc86cf39001cd", "name": "国内焦点" }, { "channelId": "5572a108b3cdc86cf39001ce", "name": "国际焦点" } ])) break; default: res.end('404') break; }})app.listen(8080);
8️⃣ 绝美宋词
相当于是使用Vue
做一个搜索功能:
代码:
<body> <div id="app"> <h1 style="text-align: center">输入关键字,找一首词</h1> <!-- TODO:待补充代码 --> <div class="search-form"> <input @input="search" v-model="val" type="text" id="search" class="search" placeholder="词牌名 词句 词人"/> <ul class="suggestions"> <li v-for="item in showList" :key="item.title"> <span class="poet" v-html="highlight(item.poetry_content)"></span> <span class="title" v-html="highlight(item.title) + '-' + highlight(item.author)"></span> </li> </ul> </div> </div> <script> let vm = new Vue({ el:'#app', // TODO:待补充代码 data:{ val:'', // 输入内容 list:[], // 源数据 showList:[] // 进行展示的数据 }, created(){ // 获取数据 axios.get("./data.json").then(res=>{ this.list = res.data }) }, methods:{ // 搜索函数 search(){ if (this.val) { // 输入内容不为空,使用filter过滤 this.showList = this.list.filter(item=>{ return item.poetry_content.includes(this.val) || item.title.includes(this.val) || item.author.includes(this.val) }) }else { // 输入内容为空,重置数据 this.showList = [] } }, // 替换关键字进行高亮的函数 highlight(str){ let reg = new RegExp(this.val,'g'); // replace第二个参数中$&代表插入匹配的子串。 return str.replace(reg, `<span class="highlight">$&</span>`) } } }) </script></body>
因为需要将关键字包上一层<span class="highlight"></span>
来进行高亮,所以我使用了v-html
指令来确保数据能以html
的格式进行渲染,并配合replace
替换关键字。
9️⃣ 平地起高楼
相当于是一道算法题,将一维数据转成树形结构,源数据:
[ { id: "51", // 区域 id name: "四川省", // 区域名字 pid: "0", // 区域的父级区域 id }, { id: "5101", name: "成都市", pid: "51", // 成都的父级是四川省,所以 pid 是 51 }, // ...];
转换成树结构:
[ { id: "51", // 地址 id name: "四川省", // 地址名 pid: "0", // 该地址的父节点 id children: [ { id: "5101", name: "成都市", pid: "51", children: [ { id: "510101", name: "市辖区", pid: "5101", children: [], // 如果该区域节点没有子集,children 则为空数组!!! }, // ... ], }, // ... ], }, // ...];
这题说复杂也复杂,说简单也简单,关键在于你怎么想了,想复杂的话能写几十行代码,想简单的话几行代码即可,我这里使用递归的方式进行解答。
首先需要先知道,convertToTree
函数接收的参数regions
代表一维数据数组,rootId
代表树形结构中根节点的pid
。convertToTree
函数返回的是指定根节点(pid=rootId
的)的树结构,所以我们只需逐渐降低rootId
,递归调用convertToTree
函数不断获取下一层的树形结构即可。
代码:
function convertToTree(regions, rootId = "0") { // TODO: 在这里写入具体的实现逻辑 // 将平铺的结构转化为树状结构,并将 rootId 下的所有子节点数组返回 // 如果不存在 rootId 下的子节点,则返回一个空数组 let arr = []; for (let i = 0; i < regions.length; i++) { if (regions[i]['pid'] === rootId) { regions[i].children = convertToTree(regions,regions[i]['id']); arr.push(regions[i]) } } return arr}module.exports = convertToTree; // 检测需要,请勿删除
从整个过程来看,convertToTree
函数执行一次就找到了一层数据,每一个数据被找到时就开始以该数据为根节点去递归调用convertToTree
函数找下一层的数据。每一次调用convertToTree
函数就会遍历一遍regions
数组,如果最终的树形结构有三层,那么就需要遍历三遍regions
数组。
如果你不想定义新的变量(如上面定义的arr
)或者想炫技,你可以使用数组的reduce方法进行递归,说到这你可能会有疑问:reduce
不是用来求和的吗?如果单纯的将reduce
归类于求和函数,你的知识面就太过单薄了。
先来看看怎么使用reduce
解答吧:
function convertToTree(regions, rootId = "0") { // TODO: 在这里写入具体的实现逻辑 // 将平铺的结构转化为树状结构,并将 rootId 下的所有子节点数组返回 // 如果不存在 rootId 下的子节点,则返回一个空数组 return regions.reduce((res,current)=>{ if (current['pid'] === rootId) { current.children = convertToTree(regions,current['id']); res.push(current); } return res; },[])}module.exports = convertToTree; // 检测需要,请勿删除
reduce
的第二个参数是一个空数组,所以:
res=[]
,current=regions[0]
。然后进行判断,如果current
是根节点的话就以current
的id
作为下一层根节点pid
递归调用convertToTree
得到下一层的数据赋值给current.children
,之后将current
添加进res
中,随后return
出res
。第二次执行时,res
为第一次执行返回的数组,current=regions[1]
第三次执行时,res
为第二次执行返回的数组,current=regions[2]
… 使用reduce
跟使用for
循环原理一样,只是看上去会给人一种很高级的感觉。
? 收快递了
这一题使用的是上一题我们转换后的树形结构:
[ { id: "51", // 地址 id name: "四川省", // 地址名 pid: "0", // 该地址的父节点 id children: [ { id: "5101", name: "成都市", pid: "51", children: [ { id: "510101", name: "市辖区", pid: "5101", children: [], }, // ... ], }, // ... ], }, // ...];
要求是:
输入"市辖区"时,返回 [ “四川省”, “成都市”, “市辖区” ]。输入"成都市", 则返回 [ “四川省”, “成都市” ]。输入"四川省", 则返回 [ “四川省” ]。如果不存在该地址,则返回一个null
。 我是思路是:
先递归遍历获取到指定name
对象的pid
。(相当于是从上向下找)再根据此pid
与父对象id
相对应的关系递归查询父对象。查询到父对象后更新pid
,并保存父对象的name
字段,然后开始新一轮的递归。(这时相当于是从下向上找) function findRegion(regions, regionName) { // TODO: 在这里写入具体的实现逻辑 // 需要从树状结构的行政信息中,遍历找到目标区域的行政信息,如输入:成都市,返回 [四川省,成都市] // 如果所输入的位置信息不存在,则返回 null let arr = [],pid; // 根据name获取字对象的pid function getPid(list) { for (let i = 0; i < list.length; i++) { if (list[i].name === regionName) { arr.push(list[i].name) // 查询到pid了 pid = list[i].pid return }else if (list[i].children.length > 0) { getPid(list[i].children) } } } // 根据pid查询父对象 function addfName(list,pfid) { for (let i = 0; i < list.length; i++) { if (list[i].id === pfid) { arr.push(list[i].name) pid = list[i].pid if (pid !== '0') { // 表示还没到根节点,更新pid后从regions开始新的递归查询 addfName(regions,pid) } return }else if (list[i].children.length > 0) { addfName(list[i].children,pid) } } } getPid(regions) addfName(regions,pid) return arr.length > 0 ? arr.reverse() : null}module.exports = findRegion; // 检测需要,请勿删除
向arr
数组 push name
的过程是从树的底层向顶层进行的,所以最后得到的arr
顺序是反的,需要reverse
反向以下。
这种解法性能消耗较大,在比赛有限的时间中也想不到好的替换方法(因为博主是个算法菜鸟?),如果大佬们有好的解法,欢迎在评论区或加入我们的交流群进行交流。
下面是职业院校组与大学组不一样的几个题:
? 偷梁换柱(职业院校组)
考察了数据拦截,可以使用Object.defineProperty
或者 Proxy
,要求:
使用Object.defineProperty:
// 请不要更改这个对象里面的内容let person = { age: 0,};// TODO:在这里写入具体的实现逻辑// 对 person 的 age 属性更新行为进行拦截// 如果输入的年龄在 0 - 150 之间,则认为是合法// 否则,如果小于 0,则返回 0;如果大于 150,则返回 150function defineReactive(obj, key, value) { Object.defineProperty(obj, key, { get() { return value; }, set(newVal) { if (newVal !== value) { newVal > 0 ? newVal > 150 ? (value = 150) : (value = newVal) : (value = 0) } } })}defineReactive(person,'age',person.age)module.exports = person; // 检测需要,请勿删除
注意,千万不要直接这样写:
Object.defineProperty(person,'age',{ set:(newVal)=>{ newVal > 0 ? newVal > 150 ? (person.age = 150) : (person.age = newVal) : (value = 0) }, get:()=>{ return person.age }})
直接这样写会陷入死循环,因为在Setter
中访问了person.age
,这又会导致触发Getter
并且对person.age
赋值又会触发person.age
,一直触发下去,完全就是一个死循环,这也就是为什么我们在上面的代码块中套了一层defineReactive
函数的原因。
在defineReactive
函数中的value
相当于是闭包中的变量,它其实并不是真正的person.age
,所以对value
的一切操作都不会导致死循环。
使用Proxy:
// 请不要更改这个对象里面的内容let person = { age: 0,};// TODO:在这里写入具体的实现逻辑// 对 person 的 age 属性更新行为进行拦截// 如果输入的年龄在 0 - 150 之间,则认为是合法// 否则,如果小于 0,则返回 0;如果大于 150,则返回 150person = new Proxy(person,{ get: function(obj, key) { return obj[key]; }, set: function(obj, key, value) { value > 0 ? value > 150 ? (obj[key] = 150) : (obj[key] = value) : (obj[key] = 0) }})module.exports = person; // 检测需要,请勿删除
? 大电影(职业院校组)
要求实现一个收藏的功能:
点击收藏图标,收藏图标在空心(images/hollow.svg
)和实心 (images/solid.svg
)中进行切换。点击收藏图标后,仅在收藏图标为实心图形时,成功提示框(id=toast__container
,原题中说的是class=toast__container
,但实际是id
而不是class
)元素显示,2 秒后该提示框自动隐藏或者点击提示框上面的关闭按钮(class=toast__close
)该提示框隐藏。使用 display
属性设置元素的显示隐藏。 完成后,最终页面效果如下:
代码:
// TODO:待补充代码let timer;$(".card-body-option-favorite img").each((i,t)=>{ $(t).click(function(){ if ($(this).attr('src') === './images/hollow.svg') { // 切换图片路径 this.src = "./images/solid.svg" // 显示弹窗 $('#toast__container').show() // 添加定时器,两秒后关闭弹窗 timer = setTimeout(()=>{ $('#toast__container').hide() },2000) } else { this.src = "./images/hollow.svg" } })})// 点击弹窗的关闭按钮$('.toast__close').click(function () { $('#toast__container').hide() if (timer) { clearTimeout(timer) }})
? 乾坤大挪移心法(职业院校组)
这时一道很常见的循环调用的题,要求如下:
mentalMethod
需要返回一个函数,可以一直进行调用,但是最后一次调用不传参。
函数通过以下方式执行,返回结果均为 '战胜峨眉,武当,少林'
(注意逗号为英文逗号)。
mentalMethod('峨眉')('武当')('少林')();mentalMethod('峨眉','武当')('少林')();mentalMethod('峨眉','武当','少林')();
代码:
function mentalMethod(...args) { // TODO 待补充代码 let a ='' a += args.join(',') let fn = function (...rest) { if (rest.length > 0) { // 如果原本a有值,需要在加新值之前添加一个,分割 a += a.length > 0 ? ',' + rest.join(',') :rest.join(','); // 继续返回fn这个函数 return fn }else { // 没有参数代表是最后一次调用,这时直接返回结果 return '战胜' + a } } return fn}
这题是利用了闭包,在外界调用fn
函数时能够使用函数mentalMethod
内层的变量。
? 不能说的秘密(职业院校组)
题目要求实现一个随机密码生成器,完善 generatePassword.js
中的 generatePassword
函数,实现根据规则随机生成密码的功能。密码长度已由 input
框(id=passwordLength
)的属性进行了限制最小 4,最大 20。
最终效果:
思路:
首先需要理解好题目的要求,密码长度最小为4,为什么最小为4呢?因为题目还要求生成的密码必须包含已选中的选项,而题中给的选项正好有4个,这一点很重要。根据用户的配置生成一个字典数组,数组中存的是密码可能所含有的所有字符。根据长度进行遍历,一次次的随机向字典数组里取一个字符添加到密码字符串中。代码:
/** * @function_name generatePassword ->生成密码的函数 * @param {*} lower 是否小写 * @param {*} upper 是否大写 * @param {*} number 是否是数字 * @param {*} symbol 是否是特殊符号 * @param {*} length 密码长度 * @return {*} string */function generatePassword(lower, upper, number, symbol, length) { //TODO:待补充代码 // 特殊字符 let sy = '!@#$%^&*(){}[]=<>/,.'; // 存放字典的数组 let arr = []; // 密码结果 let str = ''; // 向str中添加字符的函数,list代表字典。 function addStrItem(list) { // 表示从list中随机选一个字符添加到str中 str += list[Math.floor(Math.random()*list.length)] } // 添加大写字母 if (upper) { // 生成全部大写字母数组 // Array(26)表示生成长度为26的空数组 // fill用来向数组中填充内容,不填充内容是无法正常使用数组遍历的方法的 let upperList = Array(26).fill('').map((item,index) => { return String.fromCharCode(index + 65) }); // 添加到字典中 arr.push(...upperList) // 此时就在所有大写字母中随机选一个添加到str中,确保了该选项对应的值在密码中存在。 addStrItem(upperList) } // 添加小写字母 if (lower) { let lowerList = Array(26).fill('').map((item,index) => { return String.fromCharCode(index + 97) }) arr.push(...lowerList) addStrItem(lowerList) } // 添加数字 if(number) { let numberList = Array(10).fill('').map((item,index)=>index) arr.push(...numberList); addStrItem(numberList) } if(symbol){ letsymbolList = sy.split('') arr.push(...letsymbolList); addStrItem(letsymbolList) } // 添加剩余长度的字符 while (str.length < length) { addStrItem(arr) } return str}
静态 String.fromCharCode() 方法返回由指定的 UTF-16
代码单元序列创建的字符串。
UTF-16
代码单元序列为97-122大写字母的 UTF-16
代码单元序列为65-90 ? 结语
至此,第十四届蓝桥杯(Web 应用开发)模拟赛 2 期的题解就结束了,这期模拟赛整体上来说并不算很难,但考察的知识点还是比较多的,特别是对基础知识以及常见算法的考察(相信你在做题的过程中也能察觉到),所以博主还是建议大家在做题的过程中好好总结,好好复习,祝大家都能在正式比赛中取得满意的成绩!
记录一下考试成绩,因各种原因导致显示的解题时间有误,本人实际是做了大概三个小时左右,大学组十个题满分才150,最后得出的178分应该也是受解题时间的影响。
总结来说,比较耗时的题就是第6题了,大家可以注意一下,在正式比赛时做好规划。
如果本篇文章对你有所帮助,还请客官一件四连!❤️