文章目录
1. 结论
先说结论,web组难度分布一般为前6~7题为简单,基础扎实就没啥问题。最后三道题个人觉得也没啥难度,但是实现过程相对复杂(我还是太拉了,比赛的时候居然没来得及写完)。
因为是所有本科大学一起比,作为双非文科院校的渣滓,出成绩前有点担心新疆其他两所211高校可能存在高手抢夺省一道名额。不过看来我的担心还是多余了,拿了省一(顺手摘了第一☝️),下面来复盘一下这次的省赛。
2. 总体分析
试题序号、试题名称及基础源代码文件夹名称对应如下:
- 精英云课堂(5 分)
- 沉浸阅读(5 分)
- 二维码生成(10 分)
- 图形设计工具(10 分)
- 欧洲杯顶级球员数据分析(15 分)
- 内存优化之一键清理垃圾文件(15 分)
- 新闻中心(20 分)
- Github 身份验证(20 分)
- 大事件活动日历(25 分)
- 批量导入(25 分)
从难题的分布可以看出,从第5~6道Node的题目开始,就进入了中等题的难度范围。
比较麻烦的几道分别是:
- 内存优化之一键清理垃圾文件
- 新闻中心
- Github 身份验证
- 大事件活动日历
- 批量导入
先来大致讲讲这几道题目考了什么吧。
- 内存优化这道题我大致记得主要考察的是
Node.js
的文件操作fs模块
,而且和之前差不多,考察了文件夹的递归。
我记得当时看到这道题时我的嘴角不自觉的上扬了,因为这道题在近三年的国赛还是省赛题目中考过基本一样思路的。
- 新闻中心这道题目考察了
Ajax请求
和数据的处理。数据处理实际上只要JS基础扎实就没啥问题。相对来说比较常规。 - Github身份验证这道题目我记得就比较清楚了,第一问考察了随机数的生成也就是
Math.random()
函数的使用;第二问考的是Pinia
传值;第三问没什么难度。 - 大事件活动日历这道题目,考察了
ElementPlus框架
的使用,并且涉及了Vue具名插槽
的使用方法。第三问则考了计算组件Computed
。 - 批量导入是最后一道题目,也是最不好写的一道。考察的也很纯粹,就是
Js
的综合考察。要求也很明显,想要快速做出这道题必须有较强的用Js
处理数组、对象等大量数据的能力。还要能够快速理解题目的要求。
根据这些题目可以整理出本次难题的标签列表:Echart
、fs模块
、Ajax请求
、Js数据处理
、ElementPlus
、Vue3
、Pinia
、Js.Math类
。
3. 题目解析
下面贴出来的代码不一定正确,是我比赛的时候自己写的,仅供参考。
3.1 精英云课堂
TODO代码:
/* TODO:待修改代码 START*/
.container {
flex-grow: 1;
margin: 20px;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
display: flex;
}
main {
flex: 1;
padding: 20px;
background-color: #ffffff;
border-radius: 8px;
margin: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.left-sidebar, .right-sidebar {
background-color: #ffffff;
padding: 20px;
width: 220px;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
border-radius: 8px;
margin: 10px;
}
/* TODO:待修改代码 END */
很简单,会flex
布局 就行了。
3.2 沉浸阅读
js/index.js
中的代码:
function HideDom() {
// TODO:待补充代码
const buttons = document.querySelectorAll('.panel-btn')
const recommend = document.querySelector('#recommend')
const authorblock = document.querySelector('#authorblock')
let flag = (buttons[0].style.display == '' || buttons[0].style.display == 'block');
if (flag) {
for (let i = 0; i < 4; i++) {
let btn = buttons[i];
btn.style.display = 'none'
recommend.style.display = 'none'
authorblock.style.display = 'none'
}
} else {
for (let i = 0; i < 4; i++) {
let btn = buttons[i];
btn.style.display = 'block'
recommend.style.display = 'block'
authorblock.style.display = 'block'
}
}
}
比较简单,会写Javascript
就行。
3.3 二维码生成器
js/index.js
中的代码:
function generateQRCode() {
// 获取文本输入框的值并去除前后空格
const text = document.getElementById('text-input').value.trim();
// 获取二维码容器元素
const qrcode = document.getElementById('qrcode');
let tip = document.querySelector('.tip');
// 获取二维码容器的外层容器
const qrcodeContainer = document.querySelector('.qrcode-container');
// 获取错误信息容器
const errorMessageContainer = document.querySelector('.input-container');
tip.style.display = 'none';
// 检查并移除可能存在的错误信息
// TODO:目标 1
let hasErr = errorMessageContainer.lastChild.tagName == 'P';
if (text == '' && !hasErr) {
const p = document.createElement('p');
p.innerText = '请输⼊⽂本内容'
p.style.color = '#ff0000';
errorMessageContainer.appendChild(p)
} else if (text != '' && hasErr) {
let errNode = errorMessageContainer.lastChild;
errorMessageContainer.removeChild(errNode);
}
// TODO:目标 1 END
if (text.length === 0) {
return;
}
// TODO:目标 2
const strLength = text.length;
let qrsize = 1; // TODO 待修改代码 目标 2
// 设置容器样式
if (strLength*20 <= 200) {
qrsize = 200
} else if (strLength*20 <= 300) {
qrsize = (strLength*20)
} else {
qrsize = 300
}
qrcodeContainer.style.width = qrsize + 'px'
qrcodeContainer.style.height = qrsize + 'px'
if (strLength > 5) {
qrcodeContainer.style.border = '6px solid #ff69b4'
} else {
qrcodeContainer.style.border = '6px solid #00bfff'
}
// TODO:目标 2 END
// 创建 QRious 实例
const qr = new QRious({
element: qrcode,
value: text,
size:qrsize,
background: "white"
});
}
个人感觉相比前两题繁琐度上去一些,但整体还是没啥难度🤔,考的也很简单:Javascript
、
Dom
操作。
3.4 图形设计工具
js/index.js
中的代码:
// 工具栏的所有图形
const graphicalArr = document.querySelectorAll('.graphical');
// 切换颜色
const colorArr = document.querySelectorAll('.color');
// 展示图层的画布
const canvas = document.querySelector('main');
// 图层管理器的dom
const layerManagerBox = document.getElementById('layer-manager');
class LayerManager {
constructor() {
// 图层序列
this.layers = [{
id: 1,
visible: true,
name: '图层1',
active: true,
content: []
},
{
id: 2,
visible: true,
name: '图层2',
active: false,
content: []
}
];
// id计数器
this.count = 2;
}
// 修改图层内容
createContent(content) {
// 获取当前选中的图层
const layer = this.layers.find((layer) => layer.active);
layer.content.push(content);
}
// 添加图层
addLayer(id) {
const newId = ++this.count; // 新图层的 ID
// TODO:待补充代码
let idx = this.layers.findIndex(item => item.id == id)
this.layers.splice(idx, 0, {
id: newId,
visible: true,
name: '图层'+newId,
active: false,
content: []
})
// TODO:END
this.selectLayer(newId); // 选中图层
}
// 删除图层
removeLayer(id) {
// TODO:待补充代码
if (this.layers.length == 1) return;
let idx = this.layers.findIndex(item => item.id == id)
this.layers.splice(idx, 1);
}
// 向上移动
moveLayerUp(id) {
// TODO:待补充代码
let idx = this.layers.findIndex(item => item.id == id);
this.layers.splice(idx-1, 0, this.layers[idx])
this.layers.splice(idx+1, 1)
}
// 向下移动
moveLayerDown(id) {
// TODO:待补充代码
let idx = this.layers.findIndex(item => item.id == id);
this.layers.splice(idx+2, 0, this.layers[idx])
this.layers.splice(idx, 1)
}
// 修改可见性
toggleLayerVisibility(id) {
const layer = this.layers.find((layer) => layer.id === id);
if (layer) {
layer.visible = !layer.visible;
}
}
// 选中图层
selectLayer(id) {
this.layers.forEach((layer) => {
layer.active = layer.id === id;
});
}
}
// 图层管理器实例
const layerManager = new LayerManager();
// 渲染页面
function renderLayers() {
layerManagerBox.innerHTML = '';
canvas.innerHTML = '';
// 渲染图层管理器
layerManager.layers.forEach((layer) => {
// 图层卡片
const layerDiv = document.createElement('div');
layerDiv.classList.add('layer-card');
layer.active && layerDiv.classList.add('layer-card_active');
layerDiv.addEventListener('click', () => {
layerManager.selectLayer(layer.id);
renderLayers();
});
// 修改可见性的眼睛
const eyeIcon = document.createElement('span');
eyeIcon.classList.add('visible');
eyeIcon.innerText = layer.visible ? '👁️' : '🚫';
eyeIcon.addEventListener('click', () => {
layerManager.toggleLayerVisibility(layer.id);
renderLayers();
});
// 图层名称
const layerName = document.createElement('span');
layerName.classList.add('layer-name');
layerName.innerText = layer.name;
// 图层操作区域
const operatesBox = document.createElement('div');
operatesBox.classList.add('operates');
// 向上按钮按钮
const upButton = document.createElement('img');
upButton.setAttribute('src', './images/arrowToTop.png');
upButton.addEventListener('click', (e) => {
e.stopPropagation();
layerManager.moveLayerUp(layer.id);
renderLayers();
});
// 向下移动按钮
const downButton = document.createElement('img');
downButton.setAttribute('src', './images/arrowToBottom.png');
downButton.addEventListener('click', (e) => {
e.stopPropagation();
layerManager.moveLayerDown(layer.id);
renderLayers();
});
// 删除按钮
const deleteButton = document.createElement('img');
deleteButton.setAttribute('src', './images/delete.png');
deleteButton.addEventListener('click', (e) => {
e.stopPropagation();
layerManager.removeLayer(layer.id);
renderLayers();
});
// 新增按钮
const addButton = document.createElement('img');
addButton.setAttribute('src', './images/add.png');
addButton.addEventListener('click', (e) => {
e.stopPropagation();
layerManager.addLayer(layer.id);
renderLayers();
});
operatesBox.appendChild(upButton);
operatesBox.appendChild(downButton);
operatesBox.appendChild(deleteButton);
operatesBox.appendChild(addButton);
layerDiv.appendChild(eyeIcon);
layerDiv.appendChild(layerName);
layerDiv.appendChild(operatesBox);
layerManagerBox.appendChild(layerDiv);
});
//渲染图层
for (let i = layerManager.layers.length - 1; i >= 0; i--) {
const layer = layerManager.layers[i];
const layerCanvas = document.createElement('div');
layerCanvas.classList.add('layer');
if (!layer.active) layerCanvas.style.pointerEvents = 'none';
if (layer.content && layer.content.length && layer.visible) {
for (let j = 0; j < layer.content.length; j++) {
const dom = layer.content[j];
dom.onmousedown = function () {
if (!layer.active) return;
moveElement = this;
};
dom.onmouseup = function () {
moveElement = null;
};
layerCanvas.appendChild(dom);
}
}
canvas.appendChild(layerCanvas);
}
}
// 初始化渲染
renderLayers();
// 当前正在拖拽移动的dom元素
var moveElement;
for (let i = 0; i < graphicalArr.length; i++) {
const item = graphicalArr[i];
item.ondragstart = function (e) {
// console.log(e);
// offsetX;
e.dataTransfer.setData('offsetX', e.offsetX);
e.dataTransfer.setData('offsetY', e.offsetY);
moveElement = e.target.cloneNode(true);
moveElement.setAttribute('draggable', false);
};
item.ondragend = () => {};
}
// 拖入画布后实现拖拽移动
document.addEventListener('mousemove', function (e) {
setElementPosition(20, 20);
});
canvas.addEventListener('mouseleave', function (e) {
moveElement = null;
});
// 图形拖入画布后向当前选中的图层添加图形
canvas.ondrop = function (e) {
e.preventDefault();
if (!moveElement) return;
// 获取鼠标相当于正在拖拽的图形元素的偏移位置
const offsetX = +(e.dataTransfer.getData('offsetX') || 0);
const offsetY = +(e.dataTransfer.getData('offsetY') || 0);
// 设置图形相对于画布的位置
setElementPosition(offsetX, offsetY);
layerManager.createContent(moveElement);
renderLayers();
moveElement = null;
};
// 设置图形相对于画布的位置 offsetX offsetY 鼠标相对于拖拽元素的偏移量
function setElementPosition(offsetX, offsetY) {
if (!moveElement) return;
// 获取鼠标相对于整个窗口的坐标
const mouseX = event.clientX;
const mouseY = event.clientY;
// 获取画布在屏幕的位置
const rect = canvas.getBoundingClientRect();
// 计算鼠标相对于指定元素的坐标
const relativeX = mouseX - rect.left;
const relativeY = mouseY - rect.top;
moveElement.style.left = relativeX - offsetX + 'px';
moveElement.style.top = relativeY - offsetY + 'px';
}
canvas.ondragover = function (e) {
e.preventDefault();
};
// 选择颜色
for (let i = 0; i < colorArr.length; i++) {
const color = colorArr[i];
color.onclick = (e) => {
const val = color.getAttribute('color');
graphicalArr.forEach((element) => {
element.style.setProperty('--graphical-color', val);
});
};
}
比上一题要麻烦一些,但总体来说没有很难,本质上是在考察js数组操作
。
3.5 欧洲杯顶级球员数据分析
js/index.js
中的代码:
var myChart = echarts.init(document.getElementById('main'));
var dataList = []; // 存储 data.json 中的所有数据
var MockURL = './js/data.json';
/**
* 获取指定条件下的图表数据
* @param {Array} dataList data.json 中的所有数据
* @param {String} year 选择展示数据的年份,例:'2021'
*/
function showData(dataList, year) {
try {
// 设置图表选项
var option = {
graphic: {
type: 'image',
id: 'background',
left: 0,
top: 0,
z: -10,
bounding: 'raw',
origin: [0, 0],
style: {
image: './images/football.svg',
width: 900,
height: 600
}
},
xAxis: {
type: 'value',
min: 0,
max: 100,
axisTick: {
show: false
},
axisLabel: {
show: false
},
axisLine: {
show: false
},
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
min: 0,
max: 100,
axisTick: {
show: false
},
axisLabel: {
show: false
},
axisLine: {
show: false
},
splitLine: {
show: false
}
},
series: [{
name: 'Missed',
type: 'scatter',
data: [
[70, 67],
[88.5, 50],
[87.9, 50],
[74.5, 47.4],
[86, 70.5],
[88.5, 50]
],
itemStyle: {
color: '#808080',
opacity: 0.4
},
symbolSize: 10
},
{
name: 'Scored',
type: 'scatter',
data: [
[72.5, 44.7],
[72.1, 38.4],
[91.9, 51.7],
[75, 76.9],
[76.5, 32.5],
[90.9, 66.5]
],
itemStyle: {
color: '#0000ff',
opacity: 0.4
},
symbolSize: 10
}
]
};
// TODO:待补充代码 目标 2
let newDataList = dataList;
if (year == 'all') {
newDataList = dataList;
} else {
newDataList = dataList.filter(item => item.season == year)
}
option.series[0].data = newDataList.filter(item => item.result != "Goal").map(item => [item.X * 100, item.Y * 100])
option.series[1].data = newDataList.filter(item => item.result == "Goal").map(item => [item.X * 100, item.Y * 100])
// 使用指定的配置项和数据显示图表
myChart.setOption(option);
} catch (error) {
console.error('Error fetching data:', error);
}
}
// 根据选择年份更新图表数据
function updateData() {
// TODO:待补充代码 目标 3
const year = document.querySelector('#year').value;
showData(dataList, year)
}
(async () => {
// TODO:待补充代码 目标 1
const res = await axios.get(MockURL)
dataList = res.data;
// 将获取到的数据显示在图表中
showData(dataList, 'all')
})()
- 第一问考察
Ajax请求
,本次比赛我全用了axios
但是保险起见最好还是要会fetch请求
。 - 第二、三问考察对请求的数据进行处理,要了解数组操作和
Echart
的用法。
3.6 内存优化之一键清理垃圾文件
utils.js
中的代码:
const fs = require('fs');
const path = require('path');
/**
* 找到垃圾文件
* @param {string} dirPath 放待清理文件的文件夹
*/
function findGarbageFiles(dirPath) {
let garbageFiles = [];
// TODO:待补充代码
const files = fs.readdirSync(dirPath);
for (let file of files) {
// 文件夹或文件的路径
const filePath = path.resolve(dirPath, file)
const fileStats = fs.statSync(filePath);
const isFile = fileStats.isFile();
const isDirectory = fileStats.isDirectory();
const fileSize = fileStats.size;
if (isDirectory) {
garbageFiles.push(...findGarbageFiles(filePath))
} else if (isFile) {
if (fileSize <= 1024*1024) {
garbageFiles.push({
path: filePath,
size: fileSize
})
}
}
}
return garbageFiles;
}
/**
* 清理垃圾文件
* @param {Array} garbageFiles 待清理的文件
*/
function cleanGarbageFiles(garbageFiles) {
// TODO:待补充代码
for (let file of garbageFiles) {
const {path:filePath, size} = file;
fs.unlinkSync(filePath);
}
}
module.exports = { findGarbageFiles, cleanGarbageFiles}; // 检测需求,请勿删除
如果你会一点递归算法,这题就不难,本质就是一个暴力递归整个文件夹和下面的所有子文件。考察了fs模块
和Node.js
中的path
模块,最好要会使用path.resolve()
这类方法。
3.7 新闻中心
index.html
中的代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>新闻中心</title>
<link rel="stylesheet" href="./css/index.css">
</head>
<body>
<div id="app" v-cloak>
<header>
<h1>新闻中心</h1>
<h2>与未来同行,让我们的生活更美好</h2>
</header>
<main>
<template v-if="isLoading">
<div class="skeleton" v-for="skeleton in 3" :key="skeleton">
<p class="title"></p>
<p class="source"></p>
<p class="content"></p>
<div class="new-footer">
<span></span>
<span></span>
</div>
</div>
</template>
<template v-else>
<div class="new-item" v-for="(item,index) in newsList" :key="index" :title="item.title" :source="item.source"
:content="item.content" :author="item.author" :date="item.createTime">
<p class="title">{{ item.title}}</p>
<p class="source">来源:{{ item.source }}</p>
<p class="content">{{ item.content }}</p>
<div class="new-footer">
<span>{{ item.author }}</span>
<span>{{ item.createTime }}</span>
</div>
</div>
</template>
</main>
<transition name="notify">
<div class="notify" v-if="retryNewsList.length">
<p>获取到了新的新闻,是否载入?</p>
<button class="confirm" @click="onInsertData">确定</button>
</div>
</transition>
</div>
<script src="./lib/axios.min.js"></script>
<script src="./lib/axios-mock-adapter.min.js"></script>
<script src="./lib/mock.js"></script>
<script src="./lib/vue@next.js"></script>
<script>
const {
ref,
onMounted,
reactive,
toRefs,
watch
} = Vue;
const app = Vue.createApp({
setup() {
const newsList = ref([])
const retryNewsList = ref([])
const isLoading = ref(true)
const failedSources = [];
// TODO:待补充代码
async function getNews(requestUrls) {
const tem_list = ref([]);
const tem_retry = ref([]);
async function getUrls(urls, target_list) {
for (let url of urls) {
try {
const res = await axios.get(url);
if (res.data.code == 200) {
let process_data = res.data.data.map(res_obj => {
const title = Object.keys(res_obj).includes('name') ? res_obj.name : res_obj.title
const content = res_obj.content
let source = Object.keys(res_obj).includes('from') ? res_obj.from : res_obj.source
source = Object.keys(res_obj).includes('origin') ? res_obj.origin : res_obj.source
const author = res_obj.author
const createTime = Object.keys(res_obj).includes('date') ? res_obj.date : res_obj
.createTime
return {
title,
content,
source,
author,
createTime
}
})
target_list.value.push(...process_data);
}
} catch (error) {
tem_retry.value.push(error.response.request.responseURL)
}
}
newsList.value.sort((a, b) => new Date(b.createTime) - new Date(a.createTime));
isLoading.value = false;
}
await getUrls(requestUrls, newsList)
await getUrls(requestUrls, retryNewsList)
}
// TODO:END
// 所有新闻源的请求地址
const newsSource = ['news/caijing', 'news/shehui', 'news/keji', 'news/yule', 'news/jiaoyu', 'news/guoji']
getNews(newsSource)
// 载入加载失败,重试后获取成功的新闻
const onInsertData = () => {
newsList.value = [...newsList.value, ...retryNewsList.value]
retryNewsList.value = []
newsList.value.sort((a, b) => new Date(b.createTime) - new Date(a.createTime));
}
return {
isLoading,
newsList,
retryNewsList,
onInsertData
}
}
});
let vm = app.mount('#app');
</script>
</body>
</html>
这题很明显,比较综合考察了Vue3
。还考察了Ajax请求
,反正我是Axios
一把梭。如果我没有记错,这题难点在对多个请求失败的错误处理,当时也浪费了挺久的时间。
后面的几题做的不行,就不贴出来了。
4. 总结
总结,这次还是有点遗憾,没有全做出来。有几个方面的知识掌握的不太好:
- 多个Promise如何处理
- Js常见的错误处理方法
- Vue3框架一些函数和高级语法有所遗忘
- Pinia操作
- ElementPlus框架常见的使用情况