第十五届蓝桥杯Web开发应用赛道省/国赛部分题解

题目解析与代码实现

第一题:跨屏变形记(CSS 响应式布局)

题目要求:实现页面在不同屏幕尺寸下的布局切换,当屏幕宽度≤768px 时,容器改为垂直排列,各模块宽度自适应。

/* 响应式布局核心代码 */
@media (max-width: 768px) {
  #container {
    display: flex;          /* 弹性布局 */
    flex-direction: column; /* 垂直排列 */
    gap: 45px;              /* 模块间距 */
  }
  #banner, #news {
    width: 100%;            /* 宽度占满容器 */
  }
}

解析:利用 CSS 媒体查询(@media)检测屏幕宽度,通过 Flex 布局实现响应式排列。flex-direction: column 使子元素垂直排列,gap 属性简化 margin 间距设置。

第二题:垃圾分类(JavaScript 数据分类)

题目要求:根据垃圾类型(厨余/可回收/其他/有害)将数据分类到不同数组。

// 方法一:forEach + switch 分支
const garbagesorting = () => {
  const wasteData = waste._rawValue;
  wasteData.forEach(item => {
    switch (item.type) {
      case "厨余垃圾":   food_waste.value.push(item.txt); break;
      case "可回收垃圾": recyclable_waste.value.push(item.txt); break;
      case "其他垃圾":   other_waste.value.push(item.txt); break;
      case "有害垃圾":   harmful_waste.value.push(item.txt); break;
    }
  });
};

// 方法二:map 简洁写法
waste.value.map(item => {
  const { type, txt } = item;
  type === '厨余垃圾' && food_waste.value.push(txt);
  type === '可回收垃圾' && recyclable_waste.value.push(txt);
  type === '其他垃圾' && other_waste.value.push(txt);
  type === '有害垃圾' && harmful_waste.value.push(txt);
});

解析:两种方法均通过遍历数据实现分类。方法一逻辑清晰,适合复杂条件;方法二利用短路运算符简化代码,适合快速开发。注意数组引用类型特性,避免直接修改原数据。

第三题:神奇的滤镜(DOM 事件与样式控制)

题目要求:点击滤镜按钮时显示/隐藏对应滤镜层,并确保新显示的滤镜层在最上方。

filterTrigger.forEach(trigger => {
  trigger.addEventListener("click", (event) => {
    const target = event.target;
    const filterName = target.dataset.filterName; // 获取 data-filter-name 属性值
    const filterElement = document.querySelector(`.Filter[data-filter-name="${filterName}"]`);
    
    if (target.checked) {
      filterElement.style.display = "block"; // 显示滤镜层
      updateZIndex(filterElement); // 自定义函数:更新 Z 轴层级
    } else {
      filterElement.style.display = "none"; // 隐藏滤镜层
    }
  });
});

// 辅助函数:确保当前滤镜层在最上层
function updateZIndex(element) {
  element.style.zIndex = Date.now(); // 使用时间戳生成唯一层级值
}

解析:通过 dataset 属性获取自定义数据,避免硬编码。updateZIndex 函数利用时间戳保证层级唯一性,确保新激活的滤镜层覆盖旧层。

第四题:解密工具(递归算法与回溯)

题目要求:生成指定长度的不重复数字组合(如 max=5,count=3 时生成 [1,2,3], [1,2,4] 等)。

function getPossiblePasswords(max, count) {
  const result = [];
  // 递归函数:参数为当前数组和下一个数字起始值
  function get_arr_pwd(arr, start) {
    if (arr.length === count) { // 终止条件:数组长度达标
      result.push([...arr]); // 深拷贝避免引用污染
      return;
    }
    for (let i = start; i <= max; i++) {
      arr.push(i); // 选择当前数字
      get_arr_pwd(arr, i + 1); // 递归下一层,起始值+1保证不重复
      arr.pop(); // 回溯:撤销当前选择
    }
  }
  get_arr_pwd([], 1); // 从数字 1 开始生成
  return result;
}

解析:典型的组合生成问题,使用回溯算法实现。start 参数确保数字递增不重复,arr.pop() 实现回溯,避免重复计算。

第五题:项目语言占比分析器(Node.js 文件系统操作)

题目要求:遍历指定目录,统计不同扩展名文件的大小占比(区分指定类型与其他类型)。

const fs = require('fs');
const path = require('path');

function scanFiles(directoryPath, fileExtensionArr) {
  if (!fs.existsSync(directoryPath)) return `目录路径 ${directoryPath} 不存在`;
  if (!Array.isArray(fileExtensionArr)) return 'fileExtensionArr 必须是数组类型';

  const resultMap = {};
  let totalSize = 0;

  function deep(url) {
    const files = fs.readdirSync(url);
    files.forEach(item => {
      const fullPath = path.resolve(url, item);
      const stats = fs.statSync(fullPath);
      if (stats.isDirectory()) {
        deep(fullPath); // 递归处理子目录
      } else {
        const ext = path.extname(item).substring(1); // 获取扩展名(如 .js 转为 js)
        const category = fileExtensionArr.includes(ext) ? ext : 'other'; // 分类为指定类型或其他
        const size = stats.size;
        totalSize += size;
        // 累加文件大小
        resultMap[category] = resultMap[category] 
          ? { filename: category, percentage: resultMap[category].percentage + size } 
          : { filename: category, percentage: size };
      }
    });
  }

  deep(directoryPath);

  // 计算百分比并格式化结果
  return Object.values(resultMap).map(item => ({
    filename: item.filename,
    percentage: ((item.percentage / totalSize) * 100).toFixed(2)
  }));
}

解析:通过递归遍历目录(fs.readdirSync + 递归函数 deep),利用 path.extname 解析文件扩展名,按指定类型分类统计。注意处理 totalSize 避免浮点数精度问题。

第六题:学生探览(Vue 3 + ECharts 图表)

题目要求:基于 Vue 实现学生信息搜索与雷达图对比(个人成绩 vs 班级平均分)。

// Vue 组件核心逻辑(setup 语法糖)
import { ref, computed, onMounted } from "vue";
import axios from "axios";
import echarts from "echarts";

export default {
  setup() {
    const historyPrompts = [/* 搜索提示词数组 */];
    const searchText = ref("");
    const itemIndex = ref(-1);
    const items = ref([]);
    const chartDiv = ref(null);

    // 计算属性:学生姓名列表
    const names = computed(() => items.value.map(it => it.name));
    
    // 计算属性:当前学生信息
    const item = computed(() => itemIndex.value >= 0 ? items.value[itemIndex.value] : undefined);
    
    // 计算属性:班级平均分(三门科目)
    const classAverageScores = computed(() => {
      if (items.value.length === 0) return [0, 0, 0];
      return items.value.reduce((acc, cur) => {
        acc[0] += cur.scores[0]; // 技术能力得分
        acc[1] += cur.scores[1]; // 硬实力得分
        acc[2] += cur.scores[2]; // 软技能得分
        return acc;
      }, [0, 0, 0]).map(v => Math.round(v / items.value.length));
    });

    // 生命周期钩子:初始化数据
    onMounted(async () => {
      const res = await axios.get("./mock/data.json");
      items.value = res.data;
    });

    function onSearch() {
      itemIndex.value = items.value.findIndex(it => it.name === searchText.value);
      if (!item.value) return;

      // 延迟渲染图表,确保 DOM 已更新
      setTimeout(() => {
        const chart = echarts.init(chartDiv.value);
        chart.setOption({
          tooltip: {},
          legend: { data: ["我的表现", "班级平均表现"] },
          radar: {
            shape: "circle",
            indicator: [ // 雷达图指标
              { name: "技术能力得分", max: 100 },
              { name: "硬实力得分", max: 100 },
              { name: "软技能得分", max: 100 },
            ],
          },
          series: [
            {
              type: "radar",
              data: [
                { value: item.value.scores, name: "我的表现" },
                { value: classAverageScores.value, name: "班级平均表现" },
              ],
            },
          ],
        });
      }, 0);
    }

    return { /* 暴露模板所需变量与方法 */ };
  },
};

解析:利用 Vue 计算属性实现响应式数据联动,ECharts 雷达图对比个人与班级平均分。注意 setTimeout 处理异步渲染,避免图表初始化时 DOM 未加载完成。

第七题:购物狂欢节(Pinia 状态管理)

题目要求:使用 Pinia 实现购物车功能,包括添加商品(重复则数量+1)、删除商品、清空购物车及总价计算。

// store/products.js
import { defineStore } from "pinia";

export const useProductsStore = defineStore("products", {
  state: () => ({ products: [] }),
  actions: {
    async fetchProducts(category) {
      const res = await fetch(`/api/products/${category}.json`);
      this.products = await res.json(); // 更新商品列表
    },
  },
});

// store/cart.js
export const useCartStore = defineStore("cart", {
  state: () => ({ products: [] }),
  actions: {
    addProduct(product) {
      const existing = this.products.find(p => p.id === product.id);
      existing ? existing.quantity++ : this.products.push({ ...product, quantity: 1 });
    },
    removeProduct(productId) {
      this.products = this.products.filter(p => p.id !== productId); // 过滤删除
    },
    clearCart() {
      this.products = []; // 清空数组
    },
  },
  getters: {
    totalPrice() {
      return this.products.reduce((total, p) => total + p.price * p.quantity, 0); // 计算总价
    },
  },
});

解析:Pinia 的 state 存储响应式数据,actions 处理业务逻辑,getters 实现计算属性。添加商品时通过 find 检测重复项,总价计算使用数组 reduce 方法。

第八题:个性化桌面(本地存储与拖拽功能)

题目要求:实现背景图切换、新建文件、文件排序及拖拽排序功能。

// 背景图切换与本地存储
const changeBgc = (url) => {
  localStorage.setItem("bgcUrl", url); // 保存背景图 URL
  currentUrl.value = localStorage.getItem("bgcUrl"); // 更新当前显示
};

// 生成随机文件名(3位随机字符 + .txt)
function generateRandomString(length = 3) {
  const chars = "abcdefghijklmnopqrstuvwxyz";
  return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
}

// 新增文件并保存到本地存储
const addFile = () => {
  const randomName = `${generateRandomString()}.txt`;
  const newFile = {
    name: randomName,
    content: "",
    createdDate: new Date().toISOString().slice(0, 19).replace("T", " "), // 格式化时间
  };
  fileList.value.push(newFile);
  localStorage.setItem("fileList", JSON.stringify(fileList.value)); // 序列化存储
};

// 拖拽功能实现
let dragStartFile = null;

const handleDragStart = (event, item, index) => {
  dragStartFile = { item, index };
  event.dataTransfer.setData("text/plain", index); // 存储拖拽索引
};

const handleDrop = (event) => {
  event.preventDefault();
  const targetIndex = parseInt(event.dataTransfer.getData("text/plain"));
  const { item, index } = dragStartFile;
  
  // 数组移动元素
  fileList.value.splice(index, 1);
  fileList.value.splice(targetIndex, 0, item);
  
  localStorage.setItem("fileList", JSON.stringify(fileList.value)); // 更新存储
  dragStartFile = null; // 重置拖拽状态
};

解析:利用 localStorage 实现数据持久化,拖拽功能通过 dragstartdrop 事件结合数组 splice 方法实现元素位置交换。注意处理数据类型转换(如索引转为字符串存储)。

你可能感兴趣的:(前端)