第十四届蓝桥杯Web应用与开发省赛题解
- 第十四届蓝桥杯(Web 应用开发)模拟赛 3 期-大学组
- 第十四届蓝桥杯(Web 应用开发)模拟赛 3 期-职业院校组
- 第十四届蓝桥杯(Web 应用开发)模拟赛 2 期-大学组
- 第十四届蓝桥杯(Web 应用开发)模拟赛 2 期-职业院校组
- 第十三届蓝桥杯大赛(Web 应用开发)国赛-职业院校组
- 第十三届蓝桥杯大赛(Web 应用开发)省赛-职业院校组
01 电影院排座位(5 分)
.seat-area {
margin-top: 50px;
display: grid;
grid-template-columns: repeat(2, 45px 65px 45px 45px);
gap: 10px;
}
02 图⽚⽔印⽣成(5 分)
function createWatermark(text, color, deg, opacity, count) {
// 创建水印容器
const container = document.createElement('div')
container.className = 'watermark'
// TODO: 根据输入参数创建文字水印
for (let i = 0; i < count; i++) {
const span = document.createElement('span')
span.innerText = text
span.style.color = color
span.style.transform = `rotate(${deg}deg)`
span.style.opacity = opacity
container.appendChild(span)
}
return container
}
03 收集帛书碎⽚(10 分)
function collectPuzzle(...puzzles) {
// TODO: 在这里写入具体的实现逻辑
return Array.from(new Set(puzzles.flat()))
}
04 ⾃适应⻚⾯(10 分)
@media (max-width: 800px) {
.menu {
height: 54px;
line-height: 54px;
margin-bottom: 25px;
}
.icon-menu {
color: #a0a0a0;
margin-left: 20px;
display: inline-block !important;
}
.icon-menu:hover {
color: white;
cursor: pointer;
}
.collapse {
display: none;
}
input[type='checkbox']:checked ~ .collapse {
display: flex;
flex-direction: column;
background-color: #252525;
}
.dropdown:hover ul {
display: flex;
flex-direction: column;
}
.row {
margin-top: 20px;
display: flex;
flex-wrap: wrap;
}
.box {
margin-bottom: 15px;
}
#tutorials img {
margin: 0;
}
}
05 外卖给好评(15 分)
<template>
<div class="block">
<span class="demonstration">请为外卖评分:</span>
<ul class="rate-list">
<li>
<!-- TODO: 补全 el-rate 属性 -->
送餐速度:
<el-rate @change="changeScore" show-score="true" v-model="speed"></el-rate>
</li>
<li>
<!-- TODO: 补全 el-rate 属性 -->
外卖口味:
<el-rate @change="changeScore" show-score="true" v-model="flavour"></el-rate>
</li>
<li>
<!-- TODO: 补全 el-rate 属性 -->
外卖包装:
<el-rate @change="changeScore" show-score="true" v-model="pack"></el-rate>
</li>
</ul>
</div>
</template>
<script>
module.exports = {
data() {
return {
speed: 0, // 送餐速度
flavour: 0, // 外卖口味
pack: 0, // 外卖包装
}
},
/* TODO:待补充代码 */
methods: {
changeScore() {
if (this.speed && this.flavour && this.pack) {
this.$emit('change', {
speed: this.speed,
flavour: this.flavour,
pack: this.pack,
})
}
},
},
}
</script>
06 视频弹幕(15 分)
发送按钮点击事件
document.querySelector('#sendBulletBtn').addEventListener('click', () => {
// TODO:点击发送按钮,输入框中的文字出现在弹幕中
bulletConfig.value = document.querySelector('#bulletContent').value
renderBullet(bulletConfig, videoEle, true)
})
renderBullet()
/**
* @description 根据 bulletConfig 配置在 videoEle 元素最右边生成弹幕,并移动到最左边,弹幕最后消失
* @param {Object} bulletConfig 弹幕配置
* @param {Element} videoEle 视频元素
* @param {boolean} isCreate 是否为新增发送的弹幕,为 true 表示为新增的弹幕
*
*/
function renderBullet(bulletConfig, videoEle, isCreate = false) {
const spanEle = document.createElement('SPAN')
spanEle.classList.add(`bullet${index}`)
if (isCreate) {
spanEle.classList.add('create-bullet')
}
// TODO:控制弹幕的显示颜色和移动,每隔 bulletConfig.time 时间,弹幕移动的距离 bulletConfig.speed
// 解构配置对象
const { isHide, speed, time, value } = bulletConfig
// 获取视频容器的宽度和高度
const { width, height } = getEleStyle(videoEle)
// 给元素添加样式
spanEle.innerText = value
spanEle.style.color = `#${getRandomNum(999999)}`
spanEle.style.position = 'absolute'
spanEle.style.top = `${getRandomNum(height)}px`
spanEle.style.left = `${width}px`
spanEle.style.display = isHide ? 'none' : 'block'
// 将配置好的SPAN标签追加到视频容器中
videoEle.appendChild(spanEle)
// 控制弹幕移动
let left = width
const timer = setInterval(() => {
left -= speed
spanEle.style.left = `${left}px`
if (left <= -spanEle.clientWidth) {
videoEle.removeChild(spanEle)
clearInterval(timer)
}
}, time)
}
07 ISBN 转换与⽣成(20 分)
// 将用户输入的带分隔符的 isbn 字符串转换只有纯数字和大写 X 字母的字符串
// 入参 str 为转换为包含任意字符的字符串
function getNumbers(str) {
// TODO: 待补充代码
return str == '' ? '' : str.match(/\d|X/g).join('')
}
// 验证当前 ISBN10 字符串是否有效
// 入参 str 为待判断的只有纯数字和大写 X 字母的字符串
function validISBN10(str) {
// TODO: 待补充代码
const reg = new RegExp(/\d{10}|(\d{9}X{1})/)
if (!reg.test(str)) return false
const num = str.split('')
if (num.length != 10) return false // 上面那个正则有问题,这里加个判断
const sum = num.slice(0, 9).reduce((prev, cur, index) => prev + Number(cur) * ++index, 0)
const remainder = sum % 11
const contrast = num[9] == 'X' ? 10 : num[9]
return contrast == remainder
}
// 将用户输入的 ISBN-10 字符串转化为 ISBN-13 字符串
// 入参 isbn 为有效的 ISBN-10 字符串
function ISBN10To13(isbn) {
// TODO: 待补充代码
const str = '978' + isbn.substring(0, 9)
const sum = str.split('').reduce((prev, cur) => {
cur = Number(cur) // 转数字格式
return (prev += cur % 2 == 0 ? cur * 3 : cur)
}, 0)
const remainder = sum % 10
const verify = remainder == 0 ? 0 : 10 - remainder
return str + verify
}
08 全球新冠疫情数据统计(20 分)
<div id="app">
<header>
<div>全球新冠疫情数据统计</div>
</header>
<main>
<!-- TODO: 请修改以下代码实现不同国家的数据展示功能 -->
<div class="title">
<h2>{{curCountry}}</h2>
</div>
<div class="boxes">
<div class="box1">
<h3>确诊</h3>
<div class="number">
<span class="font-bold">新增:{{newConfirmed}}</span>
</div>
<div class="number">
<span class="font-bold">总计:{{totalConfirmed}}</span>
</div>
</div>
<div class="box2">
<h3>死亡</h3>
<div class="number">
<span class="font-bold">新增:{{newDeaths}}</span>
</div>
<div class="number">
<span class="font-bold">总计:{{totalDeaths}}</span>
</div>
</div>
</div>
<select v-model="selectCountry">
<option :value="0">Select Country</option>
<option v-for="item in data" :value="item">{{item.Country}}</option>
<!-- 请在此渲染所有国家选项 -->
</select>
<div id="chart" style="width: 100%; height: 50vh"></div>
</main>
</div>
<script>
var vm = new Vue({
el: "#app",
methods: {
// TODO: 请修改该函数代码实现题目要求
initChart () {
// 初始化图表
this.chart = echarts.init(document.getElementById("chart"));
this.chartOptions = {
title: {
text: "全球感染人数前30国家累计确诊人数统计",
x: "center",
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
label: { show: true },
},
},
// 设置x轴数据
xAxis: {
// TODO:这里需要显示国家名称缩写,因为有些国家的全称太长,会导致界面不美观
data: this.countryCodeList,
axisLabel: {
show: true,
interval: 0,
},
},
yAxis: {
type: "value",
name: "确诊数量",
},
// 设置y轴数据
series: [
{
// TODO:设置图表中的总确证数
data: this.totalConfirmedList,
type: "bar",
itemStyle: {
normal: { color: "#a90000" },
},
},
],
};
// 调用此方法设置 echarts 数据
this.chart.setOption(this.chartOptions);
},
},
// TODO: 请在此添加代码实现组件加载时数据请求的功能
async mounted () {
this.data = (await axios.get('./js/covid-data.json')).data
this.initChart();
},
computed: {
countryCodeList () {
return this.data.map(e => e.CountryCode)
},
totalConfirmedList () {
return this.data.map(e => e.TotalConfirmed)
}
},
data () {
return {
data: [],
selectCountry: 0,
newConfirmed: 0,
totalConfirmed: 0,
newDeaths: 0,
totalDeaths: 0,
curCountry: '请选择国家'
}
},
watch: {
// 监听下拉列表值,动态切换数据
selectCountry (val) {
this.curCountry = val.CountryCode || this.curC
this.newConfirmed = val.NewConfirmed || 0
this.totalConfirmed = val.TotalConfirmed || 0
this.newDeaths = val.NewDeaths || 0
this.totalDeaths = val.TotalDeaths || 0
}
}
});
</script>
09 ⻣架屏(25 分)
实现思路: 此题主要难点在于 style 和 class 的处理,将当前渲染对象传入对应方法,判断返回对象具有的属性
/*
* 骨架屏渲染组件
*/
let ItemTemplate = ``
// TODO: 请补充完整Template,完成组件代码编写
ItemTemplate += `
<div :class="'ske-'+paragraph.type+'-container'">
<div v-for="item in arrIs(paragraph)" :class="classIs(item)" :style="styleIs(item)" >
<item :paragraph="item" :active="active"></item>
</div>
</div>
`
Vue.component('item', {
name: 'item',
template: ItemTemplate,
props: ['paragraph', 'active'],
data() {
return { typeList: ['rect', 'circle'], classPrefix: 'ske ske-', activeClass: ' ske-ani' }
},
watch: {},
methods: {
// 判断是 rows or cols
arrIs(obj) {
if (obj?.rows) return obj.rows
else if (obj?.cols) return obj.cols
else return []
},
// 判断class类
classIs(obj) {
if (this.typeList.includes(obj.type)) {
return this.classPrefix + obj.type + (this.active ? this.activeClass : '')
} else {
return this.classPrefix + obj.type
}
},
// 判断样式
styleIs(obj) {
if (obj?.style && obj?.rowStyle) return { ...obj.style, ...obj.rowStyle }
else if (obj?.style) return obj.style
else if (obj?.rowStyle) return obj.rowStyle
else if (obj?.colStyle) return obj.colStyle
else return {}
},
},
})
10 组课神器(25 分)
ajax()
async function ajax({ url, method = 'get', data }) {
let result
// TODO:根据请求方式 method 不同,拿到树型组件的数据
// 当method === "get" 时,localStorage 存在数据从 localStorage 中获取,不存在则从 /js/data.json 中获取
// 当method === "post" 时,将数据保存到localStorage 中,key 命名为 data
if (method === 'get') {
const dataList = localStorage.getItem('data')
result = dataList ? JSON.parse(dataList) : (await axios({ url, method })).data.data
}
if (method === 'post') {
// result = (await axios({ url, method, data })).data
localStorage.setItem('data', JSON.stringify(data))
}
return result
}
treeMenusRender()
实现思路: 递归生成DOM树
function treeMenusRender(data, grade = 0) {
let treeTemplate = ''
// TODO:根据传入的 treeData 的数据生成树型组件的模板字符串
grade++
for (obj of data) {
treeTemplate +=
grade === 3
? `<div class="tree-node" data-index="${obj.id}" data-grade="${grade}">
<div class="tree-node-content" style="margin-left: 30px">
<div class="tree-node-content-left">
<img src="./images/dragger.svg" alt="" class="point-svg" />
<span class="tree-node-tag">${obj.tag}</span>
<span class="tree-node-label">${obj.label}</span>
</div>
<div class="tree-node-content-right">
<div class="students-count">
<span class="number"> 0人完成</span>
<span class="line">|</span>
<span class="number">0人提交报告</span>
</div>
<div class="config">
<img class="config-svg" src="./images/config.svg" alt="" />
<button class="doc-link">编辑文档</button>
</div>
</div>
</div>`
: `<div class="tree-node" data-index="${obj.id}" data-grade="${grade}">
<div class="tree-node-content" style="margin-left: ${grade === 2 && 15}px">
<div class="tree-node-content-left">
<img src="./images/dragger.svg" alt="" class="point-svg" />
<span class="tree-node-label">${obj.label}</span>
<img class="config-svg" src="./images/config.svg" alt="" />
</div>
</div>`
if (obj?.children) treeTemplate += `<div class="tree-node-children">${treeMenusRender(obj.children, grade)}</div>`
treeTemplate += `</div>`
}
return treeTemplate
}
treeDataRefresh()
实现思路: 利用字符串替换的方式,将拖动的元素插入到对应节点,最后将替换完成的字符串转JSON对象重新赋值给 treeData
- 首先将 treeData 转字符串
- 调用 getDragElement() 方法,将被拖拽的元素对象和放入元素对象转字符串
- 根据题目要求对 treeDataStr 进行替换
- 将 treeDataStr 转JSON对象并赋值给 treeData
function treeDataRefresh({ dragGrade, dragElementId }, { dropGrade, dropElementId }) {
if (dragElementId === dropElementId) return
// TODO:根据 `dragElementId, dropElementId` 重新生成拖拽完成后的树型组件的数据 `treeData`
let dragStr = JSON.stringify(getDragElement(treeData, dragElementId))
let dropStr = JSON.stringify(getDragElement(treeData, dropElementId))
let treeDataStr = JSON.stringify(treeData)
if (dragGrade === dropGrade) {
treeDataStr = treeDataStr.replace(dragStr, '')
treeDataStr = treeDataStr.replace(dropStr, dropStr + ',' + dragStr)
}
if (dragGrade - dropGrade == 1) {
if (dropStr.includes(dragStr)) dropStr = dropStr.replace(dragStr, '')
const newDragStr = `${dragStr},`
const newDropStr = dropStr.replace('[', '[' + newDragStr)
treeDataStr = treeDataStr.replace(dragStr, '')
treeDataStr = treeDataStr.replace(dropStr, newDropStr)
}
// 处理多余字符
treeDataStr = treeDataStr.replace(',,', ',').replace('[,', '[').replace(',]', ']')
treeData = JSON.parse(treeDataStr)
}
getDragElement()
通过传入的 id 获取被拖拽的节点对象
function getDragElement(data, id) {
for (const obj of flatObj(data)) {
if (obj.id == id) return obj
}
}
flatObj()
此方法将 treeData 扁平化
function flatObj(obj) {
return obj.reduce((prev, cur) => {
prev = [...prev, cur]
if (cur?.children) prev = [...prev, ...flatObj(cur.children)]
return prev
}, [])
}