第十六届 -- 蓝桥杯Web开发大学组省赛个人复盘

1. 结论

先说结论,web组难度分布一般为前6~7题为简单,基础扎实就没啥问题。最后三道题个人觉得也没啥难度,但是实现过程相对复杂(我还是太拉了,比赛的时候居然没来得及写完)。

因为是所有本科大学一起比,作为双非文科院校的渣滓,出成绩前有点担心新疆其他两所211高校可能存在高手抢夺省一道名额。不过看来我的担心还是多余了,拿了省一(顺手摘了第一☝️),下面来复盘一下这次的省赛。

2. 总体分析

试题序号、试题名称及基础源代码文件夹名称对应如下:

  1. 精英云课堂(5 分)
  2. 沉浸阅读(5 分)
  3. 二维码生成(10 分)
  4. 图形设计工具(10 分)
  5. 欧洲杯顶级球员数据分析(15 分)
  6. 内存优化之一键清理垃圾文件(15 分)
  7. 新闻中心(20 分)
  8. Github 身份验证(20 分)
  9. 大事件活动日历(25 分)
  10. 批量导入(25 分)

从难题的分布可以看出,从第5~6道Node的题目开始,就进入了中等题的难度范围。

比较麻烦的几道分别是:

  • 内存优化之一键清理垃圾文件
  • 新闻中心
  • Github 身份验证
  • 大事件活动日历
  • 批量导入

先来大致讲讲这几道题目考了什么吧。

  1. 内存优化这道题我大致记得主要考察的是Node.js的文件操作fs模块,而且和之前差不多,考察了文件夹的递归。

我记得当时看到这道题时我的嘴角不自觉的上扬了,因为这道题在近三年的国赛还是省赛题目中考过基本一样思路的。

  1. 新闻中心这道题目考察了Ajax请求 和数据的处理。数据处理实际上只要JS基础扎实就没啥问题。相对来说比较常规。
  2. Github身份验证这道题目我记得就比较清楚了,第一问考察了随机数的生成也就是Math.random()函数的使用;第二问考的是Pinia传值;第三问没什么难度。
  3. 大事件活动日历这道题目,考察了ElementPlus框架的使用,并且涉及了Vue具名插槽的使用方法。第三问则考了计算组件Computed
  4. 批量导入是最后一道题目,也是最不好写的一道。考察的也很纯粹,就是Js的综合考察。要求也很明显,想要快速做出这道题必须有较强的用Js 处理数组、对象等大量数据的能力。还要能够快速理解题目的要求。

根据这些题目可以整理出本次难题的标签列表:Echartfs模块Ajax请求Js数据处理ElementPlusVue3PiniaJs.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框架常见的使用情况
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值