目录
第十题 NPM Download Simulator(25分)
备赛链接:备赛蓝桥杯 - 蓝桥云课
2024年十五届国赛大学组真题(共10道题)
第一题 真人鉴定器(5分)
function changeBanner(isPre) {
// TODO:待补充代码
if (isPre) {
if (thisIndex == 0) {
thisIndex = imgListLi.length - 1;
} else {
thisIndex--;
}
} else {
if (thisIndex == imgListLi.length - 1) {
thisIndex = 0;
} else {
thisIndex++;
}
}
// TODO: END
第二题 俄罗斯方块(5分)
.z-shape {
display: grid;
/* TODO:待补充代码 */
/* 2行 */
grid-template-rows: repeat(2, 30px);
/* 4列 */
grid-template-columns: repeat(4, 30px);
}
.l-shape {
display: grid;
/* TODO:待补充代码 */
/* 2行 */
grid-template-rows: repeat(2,30px);
/* 3列 */
grid-template-columns: repeat(3,30px);
}
第三题 个人消息同步(10分)
目标一:
import { defineStore } from "pinia";
import { ref } from "vue";
export const useMessageStore = defineStore("message", () => {
const messageState = ref([]);
// 定义请求地址 MockUrl
const MockUrl = "./data.json";
let getUserMessage = async () => {
// TODO:待补充代码
await axios.get(MockUrl).then((res) => {
messageState.value = res.data.data
});
// TODO:END
};
return {
messageState,
getUserMessage,
};
});
目标二:
setup() {
const messageStore = useMessageStore();
const headerData = reactive({
headerArr: ["学习", "蓝桥杯", "考证", "讨论区", "校企版"],
});
const isShowPop = ref(true);
// TODO:待补充代码 目标 2
const messageStateLen = computed(() => {
return messageStore.messageState.length;
});
return {
headerData,
messageStore,
isShowPop,
messageStateLen,
};
},
......
<! --TODO:待补充代码 目标 2-- >
<div class="tip">{{messageStateLen}}</div>
目标三:
<!-- TODO: 待修改代码 目标 3 -->
<!-- 单条消息模板 start-->
<div v-for="item in messageStore.messageState" :key="item.msg_id" class="message-content-wrapper mt-20px" >
<div class="message-list">
<div class="message-item">
<div class="message-content">
<div class="message-cate">
<span class="label label-success font-14">{{item.label}}</span>
</div>
<div class="message-main-content">
{{item.content}}
</div>
</div>
<div class="msg-create-time text-right">
<small> {{item.send_at}}</small>
</div>
</div>
</div>
</div>
<!-- 单条消息模板 end-->
第四题 工作协调(10分)
static difference(a, b) {
// TODO:待补充代码 目标 1
let dSet = new XSet()
for (let itemA of a) {
// // 判断集合b里是否不包含a,如果集合b里不包含a,则符合条件
if (Array.from(b).every(e => itemA !== e)) {
dSet.add(itemA)
}
}
return dSet // 修改此处为函数的正确返回值
}
static intersection(a, ...bSets) {
// TODO:待补充代码 目标 2
let iSet = new XSet()
for (let itema of a) {
if (bSets.every(bSet => bSet.has(itema))) {
iSet.add(itema)
}
}
return iSet// 修改此处为函数的正确返回值
}
static union(a, ...bSets) {
// TODO:待补充代码 目标 3
let uSet = new XSet()
a.size ? uSet.add(...Array.from(a)): null
for (let itemb of Array.from(bSets)) {
for (let element of itemb) {
uSet.add(element)
}
}
return uSet // 修改此处为函数的正确返回值
}
总结:对集合,数组,对象方法的用法
第五题 新手引导(15分)
// TODO:待补充代码 目标 1
introduce.style.top = y + "px";
if (isLeft) {
introduce.style.left = right + distance + "px";
} else {
// clientWidth
introduce.style.left =
left - getDomWholeRect(introduce).width - distance + "px";
}
console.log(target);
// TODO:END
function copyTarget() {
// TODO:待补充代码
// 克隆 DOM 节点
clone = target.cloneNode(true);
// 当前target的位置大小
const { top, right, bottom, left, x, y, width, height } = boundingClientRect;
console.log(target);
// 设置clone的位置和样式属性
clone.style.zIndex = 9999;
clone.style.top = y + "px";
clone.style.left = x + "px";
clone.style.position = "absolute";
clone.style.width = width + "px";
clone.style.height = height + "px";
// 添加在target上
target.parentNode.appendChild(clone);
// TODO:END
}
// 移除元素
function removeTarget() {
clone?.remove()
}
第六题 简易webpack(15分)
const buildFn = () => {
const path = require("path");
const fs = require("fs");
const webpack = require("./webpack.config"); // webpack 配置对象
// 以上代码请勿修改
// TODO:待补充代码,所有代码请写在 `buildFn` 函数内
//目标一
//解构赋值-拿到webpack对象中的全部数据
const { entry, output } = webpack;
const { path: _path, filename, publicPath, resolve } = output;
const alias = resolve.alias;
let files = fs.readFileSync(entry, "utf-8"); //用于同步读取文件内容的方法调用(内容)
for (const key in alias) {
//替换 alias 的路径,在 entry中
files = files.replace(key, alias[key]);
}
let outPath = path.resolve(_path, filename);
fs.writeFileSync(outPath, files); //将替换后的文件 files 写入到 outPath路径中
//目标二
let htmlFiles = fs.readFileSync(
path.resolve(__dirname, "index.html"),
"utf-8"
); //用于同步读取文件内容的方法调用(内容)
// 在 HTML 文件中的 src=" 或 href=" 前插入 publicPath
let newHtmlFiles = htmlFiles.replace(
//综合起来,(?<=(src|href)=") 这个后行断言的作用是:检查当前位置的前面是否是 src=" 或者 href="。如果满足这个条件,就认为找到了一个匹配项。
/(?<=(src|href)=")/g,
output.publicPath
);
fs.writeFileSync(path.resolve(_path, "index.html"), newHtmlFiles); //将替换后的文件 htmlFiles 写入到 outPath路径中
//目标三
dist_path = path.join(_path, 'static')
fs.mkdirSync(dist_path)
function dfs(path1, dis_path) {
let fileList = fs.readdirSync(path1)
for (let f of fileList) {
const fileStats = fs.statSync(path.join(path1, f));
let sourceFilePath = path.join(path1, f)
let distFilePath = path.join(dis_path, f)
if (fileStats.isDirectory()) {
fs.mkdirSync(distFilePath)
dfs(sourceFilePath, distFilePath)
}
if (fileStats.isFile()) {
fs.copyFileSync(sourceFilePath, distFilePath);
}
}
return;
}
dfs(path.resolve(__dirname, 'static'), dist_path)
};
module.exports = buildFn;
第七题 会议日程(20分)
// 创建日程
const handleSave = async (formEl) => {
if (!formEl) return;
await formEl.validate(async (valid, fields) => {
if (valid) {
const params = editState.form;
if (!params.id) {
// TODO:待补充代码 目标 1
await axios.post("/api/meetings", params);
// TODO:END
await fetchData();
showEdit.value = false;
} else {
// 编辑日程
await axios.put("/api/modify", params);
await fetchData();
showEdit.value = false;
}
} else {
console.log("error submit!", fields);
}
});
};
// 删除单个会议日程
const handleDeleteOne = async (record) => {
// TODO:待补充代码 目标 2
await axios.delete(`/api/delmeeting/${record.id}`);
// TODO:END
fetchData();
};
// 点击单个会议日程
const handleSelect = (record) => {
// TODO:待补充代码 目标 3 和 4
for (const day of list.value) {
for (const item of day.meetings) {
if (item.id === record.id) {
item.checked = !item.checked;
day.checked = day.meetings.every((item) => item.checked);
allCheckStatus.value = list.value.every((day) => day.checked);
return;
}
}
}
// TODO:END
};
// 点击日期多选框
const handleSelectDate = (item) => {
// TODO:待补充代码 目标 3 和 4
for (const day of list.value) {
if (day.id === item.id) {
day.checked = !day.checked;
day.meetings.forEach((item) => (item.checked = day.checked));
allCheckStatus.value = list.value.every((day) => day.checked);
return;
}
}
// TODO:END
};
// 点击全选选择框
const handleSelectAll = () => {
// TODO:待补充代码 目标 4
allCheckStatus.value = !allCheckStatus.value;
list.value.forEach((day) => {
day.checked = allCheckStatus.value;
day.meetings.forEach((item) => {
item.checked = allCheckStatus.value;
});
});
// TODO:END
};
第八题 代码量统计(20分)
const MockUrl = "./data.json"; // 定义请求地址
/**
* @return {Array} 请求到的数组
*/
async function fetchCodeData() {
// TODO:待补充代码 目标 1
let response = await fetch(MockUrl);
return await response.json();
}
// 根据数据绘制图表
function setChart(chart, data) {
chart.setOption({
title: {
text: "代码量统计",
left: "center",
top: 20,
},
series: [
{
name: "Code Counter",
type: "treemap",
data,
itemStyle: {
gapWidth: 5,
},
// TODO:待补充代码 目标 3
visibleMin: 6400,
label: {
// TODO:待补充代码 目标 3
formatter: "{b}\n{c}行",
},
levels: [
{
color: ["#FCB944", "#80B7C2", "#C48483", "#F0663B", "#75D180"],
},
{
colorSaturation: [0.35, 0.5],
},
],
},
],
});
}
window.onload = async () => {
const dom = document.querySelector("#canvasContainer");
const chart = echarts.init(dom);
// 获取原始数据
const rawData = await fetchCodeData();
// 处理后的数据
const processedData = [];
// TODO:待补充代码 目标 2
rawData.forEach(([pathName, codeVal], idx) => {
let splitArr = pathName.split("/");
loadData(processedData, splitArr, codeVal);
});
function loadData(pathArr, splitArr, codeVal) {
if (splitArr.length == 1) {
pathArr.push({
name: splitArr[0],
value: codeVal,
children: [],
});
} else {
for (const child of pathArr) {
if (child.name === splitArr[0]) {
splitArr.shift();
loadData(child.children, splitArr, codeVal);
}
}
}
}
// TODO:END
// 绘制图表
setChart(chart, processedData);
};
第九题 国际化适配(25分)
const t = (langKey, option) => {
// TODO:待补充代码 目标 1
let langs = { zh_CN, en_US, ja_JP };
let langKeys = langKey.split(".");
let result = langKeys[0] == "nav" ? langs[lang].nav[langKeys[1]] : langs[lang][langKey]
return result.replace(/\$\$(.*?)\$\$/g, (match,variableName)=>{
return option[variableName] || match;
// TODO:END
})
};
const resutl = {};
const urls = url.slice(url.indexOf('?')+1).split('&');
urls.forEach(e => {
let temp = e.split("=");
resutl[temp[0]] = temp[1];
});
return resutl;
watch(selectLang,(newVal,oldVal) =>{
let arr = selectLang.value.split("__");
history.replaceState({},null, `?lang=${arr[0]}&theme=${arr[1]}`);
ctx.emit('url-change')
})
第十题 GitHub Desktop(25)
// 回滚到此版本
function onRollBackClick(commitId) {
// TODO:待补充代码 目标 1
const c = this.currentBranchCommits
c.splice(0, c.findIndex(x => x.commitId == commitId))
}
// 新建分支
function onNewBranchClick() {
// TODO:待补充代码 目标 2
const i = inputNewBranch, v = i.value
with (gitState.value)
if (v && !branchList.includes(v)) {
branchCommitsObject[v] = [...branchCommitsObject[currentBranch]]
branchList.push(currentBranch = v)
i.value = ''
}
}
// 删除分支
function onBranchDelete(branchName) {
// TODO:待补充代码 目标 3
if (branchName != 'master')
with (this.gitState) {
branchList.splice(branchList.indexOf(branchName), 1)
delete branchCommitsObject[branchName]
if (currentBranch == branchName)
currentBranch = 'master'
}
}
-------------
/**
* 辅助函数,用于深拷贝对象
* @param {Object} obj
* @returns
*/
function deepClone(obj) {
if (Array.isArray(obj)) {
return obj.map((it) => deepClone(it));
} else if (typeof obj === "object") {
const result = {};
for (let key in obj) {
result[key] = deepClone(obj[key]);
}
return result;
} else {
return obj;
}
}
/**
* TODO: 待补充代码 目标 4
* @param {Ref} someRef Vue的某个ref对象,需要对传入的ref对象的历史状态做记录
* @returns 返回一个对象,其中包含函数 undo 和 redo; undo 表示撤销,比如给 someRef 设置一个新状态后,调用 undo 可以将 someRef 还原为上一个旧状态;同理,在旧状态调用 redo 可以将 someRef 恢复为新状态
*/
function useRefHistory(someRef) {
let stack = [],
flag = true,
n = (maxn = -1);
Vue.watch(
someRef,
(x) => (flag ? (stack[(maxn = ++n)] = JSON.stringify(x)) : (flag = true)),
{ deep: true, immediate: true }
);
return {
undo() {
if (n) {
flag = false;
someRef.value = JSON.parse(stack[--n]);
}
},
redo() {
if (n != maxn) {
flag = false;
someRef.value = JSON.parse(stack[++n]);
}
},
};
}
2024年十五届省赛大学组真题(共10道题)
第一题 智能停车系统(5分)
.cars {
position: absolute;
z-index: 2;
width: 600px;
height: 600px;
display: flex;
flex-direction: column;
/* 排成列*/
/* TODO: 请为下方属性填空,不要更改其他选择器里的代码 */
flex-wrap: wrap;
align-content: space-between;
justify-content: space-between;
}
第二题 布局切换(5分)
// 获取元素
const layoutContainer = document.getElementById("layoutContainer"); // 布局元素
const layoutOptions = document.querySelectorAll(".layout-option"); // 三个模式元素
const switching = document.getElementById("switching"); // 模式按钮
// 显示模式
switching.addEventListener("click", function () {
mode.style.display = "flex"; // 设置显示为flex布局
});
// 遍历选项
layoutOptions.forEach(function (option) {
// 经典模式,浏览模式,工具模式点击事件
option.addEventListener("click", function () {
// TODO:待补充代码
let imgList = document.querySelectorAll(".layout-option");
const _this = this
imgList.forEach((item) => {
if (item.id == _this.id) {
item.classList.add('active')
}
else {
item.classList.remove('active')
}
});
// TODO:END
// 以下代码无需修改
// 根据不同选项进行布局处理
if (this === layoutOptions[0]) {
// Classic mode
tool.style.display = "none"; // 隐藏工具
layoutContainer.classList.add("two-column-layout"); // 添加两列布局类
layoutContainer.classList.remove("three-column-layout"); // 移除三列布局类
} else if (this === layoutOptions[1]) {
// Browse mode
tool.style.display = "none"; // 隐藏工具
layoutContainer.classList.add("three-column-layout"); // 添加三列布局类
layoutContainer.classList.remove("two-column-layout"); // 移除两列布局类
} else if (this === layoutOptions[2]) {
// Tool mode
tool.style.display = "flex"; // 显示工具
}
mode.style.display = "none"; // 隐藏布局容器
});
});
第三题 产品360度展示(10分)
/**
* @param {*} initialValue 初始值
* @param {Array} sequence 由普通函数或 Promise 函数组成的数组
* @return {Promise}
*/
const pipeline = (initialValue, sequence) => {
// TODO: 待补充代码
// console.log(sequence[0](initialValue));
return new Promise(async (resolve, reject) => {
for (let index = 0; index < sequence.length; index++) {
initialValue = await sequence[index](initialValue);
}
resolve(initialValue);
});
};
// 检测需要,请勿删除
try {
module.exports = { pipeline };
} catch { }
第四题 多表单校验(10分)
// TODO:待补充代码
const reg = /[^\u4e00-\u9fa5]/g;
const rules = reactive({
name: {
required: true,
validator: (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入姓名,只能输入汉字"));
} else if (reg.test(value)) {
callback(new Error("只能输入汉字"));
} else {
callback();
}
},
},
sex: { required: true, message: "请选择性别" },
age: { required: true, message: "请选择性别" },
isCompetition: { required: true, message: "请输入年龄" },
isEntrepreneurship: {
required: true,
message: "请选择是否参加过编程比赛",
},
footnote: { required: true, message: "请选择是否有过创业经历" },
});
第五题 找回连接的奇幻之旅(15分)
function resetableOnce(fn) {
// TODO: 待补充代码
let result;
let done = true;
function runOnce() {
if (done) {
result = fn(...arguments);
console.log(...arguments);
done = false;
}
return result;
}
function reset() {
done = true;
}
// TODO: END
return { runOnce, reset };
}
第六题 tree 命令助手(15分)
// 生成文件树
function generateTree(dirPath) {
// TODO:待补充代码
let pathArr = fs.readdirSync(dirPath);
let result = [];
for (const path_name of pathArr) {
file_path = path.resolve(dirPath, path_name);
// file_path = path.join(dirPath, path_name);
if (fs.statSync(file_path).isDirectory()) {
result.push({
name: path_name,
children: generateTree(file_path),
});
} else {
result.push({
name: path_name,
});
}
}
return result
}
第七题 Github 明星项目统计(20分)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Github 明星项目统计</title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<link rel="stylesheet" type="text/css" href="css/style.css" />
<script src="./lib/axios.js"></script>
<script src="lib/vue.global.js"></script>
<script src="lib/echarts.min.js"></script>
<body>
<div id="app">
<!-- 图表容器 -->
<div id="chart" style="width: 100%; height: 500px;"></div>
<div class="filters">
<div>
筛选语言
<!-- TODO: 待补充代码 -->
<select name="language" id="language" @change="changeHandle" v-model="language">
<option v-for="item in languages" :value="item">{{item}}</option>
</select>
</div>
<div>展示第<input id="first" type="text" v-model="pageStart" @input="changeHandle">到第<input id="second" type="text"
v-model="pageEnd" @input="changeHandle">位的项目</div>
</div>
</div>
</body>
<script>
var xData;
var yData;
const app = Vue.createApp({
setup() {
// 定义响应式数据
const chart = Vue.ref(null);
const chartOptions = Vue.ref(null);
const chartData = Vue.ref(null);
xData = Vue.ref(null);
yData = Vue.ref(null);
const languages = Vue.ref(['All', 'JavaScript', 'TypeScript', 'Python', 'Shell', 'C++', 'C#', 'Go', 'Rust', 'Java']);
const language = Vue.ref('All');
const pageStart = Vue.ref(1);
const pageEnd = Vue.ref(100);
// 语言筛选改变时或页面数字输入框数字改变时的处理函数
const changeHandle = () => {
// TODO:待补充代码
let result = Vue.ref([])
if(language.value == 'All') {
xData.value = chartData.value.map(item => item.name);
yData.value = chartData.value.map(item => item.stars);
}else {
result.value = chartData.value.filter(item => item.language == language.value)
result.value = result.value.slice(pageStart.value - 1, pageEnd.value);
xData.value = result.value.map(item => item.name);
yData.value = result.value.map(item => item.stars);
}
initChart();
};
// 初始化图表
const initChart = () => {
chart.value = echarts.init(document.getElementById('chart'));
chartOptions.value = {
title: {
text: 'Github 明星项目统计',
x: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
label: {
show: true
}
}
},
xAxis: {
data: xData.value,
},
yAxis: {
type: 'value',
label: 'star数量'
},
series: [
{
data: yData.value,
type: 'bar',
}
],
};
chart.value.setOption(chartOptions.value);
};
// 组件挂载时获取数据
Vue.onMounted(() => {
axios.get('./js/data.json').then(res => {
chartData.value = res.data;
let newData = chartData.value.slice(pageStart.value - 1, pageEnd.value);
xData.value = chartData.value.map(item => item.name);
yData.value = chartData.value.map(item => item.stars);
initChart();
});
});
return {
chart,
chartOptions,
chartData,
xData,
yData,
languages,
language,
pageStart,
pageEnd,
initChart,
changeHandle,
};
},
});
var vm = app.mount('#app');
</script>
</html>
第八题 小蓝驿站(20分)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>小蓝驿站</title>
<!-- 链接外部样式表 -->
<link rel="stylesheet" href="css/style.css">
<!-- 引入 Vue.js 框架 -->
<script src="lib/vue.global.js"></script>
</head>
<body>
<div id="app">
<!-- 邮箱顶部标题和操作按钮 -->
<div class="email-header">
<!-- 邮箱标题 -->
<div class="email-header-logo">
<h1 class="email-header-title">小蓝驿站</h1>
</div>
<!-- 邮箱操作按钮,包括撰写邮件、设置和退出 -->
<div class="email-header-actions">
<button class="email-header-button">撰写邮件</button>
<button class="email-header-button">设置</button>
<button class="email-header-button">退出</button>
</div>
</div>
<div class="container">
<!-- 邮箱侧边栏 -->
<div class="email-sidebar">
<!-- 邮箱文件夹列表 -->
<ul class="folder-list">
<li class="folder-list-item">收件箱</li>
<li class="folder-list-item">草稿箱</li>
<li class="folder-list-item">已发送</li>
<li class="folder-list-item">已删除</li>
<li class="folder-list-item">垃圾邮件</li>
<li class="folder-list-item">广告邮件</li>
</ul>
</div>
<div>
<!-- 通讯录标题 -->
<div class="contacts-title">通讯录</div>
<!-- 添加联系人区块 -->
<div class="add-contacts">
<!-- 添加联系人标题 -->
<div class="contacts-group-title">添加联系人</div>
<!-- 添加联系人表单 -->
<div class="add-contact">
<!-- 联系人输入框,使用 Vue 的 v-model 进行双向数据绑定 -->
<input class="add-contact-input" v-model="newContact" placeholder="新联系人名字">
<!-- 添加按钮,点击后触发 addContact 方法 -->
<button class="add-contact-button" @click="addContact">添加</button>
</div>
</div>
<!-- 联系人列表 -->
<ul class="contacts-list">
<!-- TODO:待补充代码 目标 1 -->
<!-- 以 A 为例 start -->
<li class="contacts-group" v-for="item in contacts "> <!-- 字母分组渲染 DOM 结构-->
<div class="contacts-group-title">{{item.letter}}</div> <!-- 分组的 字母名称 -->
<ul>
<li class="contact-item" v-for="el in item.contacts"> <!-- contact-item 人名渲染 dom 结构-->
<span class="contact-name">{{el.name}}</span><button
class="del-contact-button">删除</button>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
<script>
// 从 Vue 对象中解构出 reactive、computed 和 ref 等方法
const { reactive, computed, ref, onMounted } = Vue;
// 定义 Vue 应用
const app = {
setup() {
// 定义并初始化联系人列表
let contacts = ref([]);
onMounted(async () => {
const res = await fetch('./data.json').then(res => res.json()).then(data => data);
contacts.value = res;;
});
// 定义并初始化新联系人名字
const newContact = ref("");
// 定义计算属性,对联系人列表进行排序
const sortedContacts = computed(() => {
return contacts.value.sort((a, b) => a.letter.localeCompare(b.letter));
});
// 定义添加联系人的方法
const addContact = () => {
// TODO:待补充代码 目标 2
let newVal = newContact.value
if (newVal != '') {
let firstLetter = newVal.charAt(0).toLocaleUpperCase();
let groupIndex = contacts.value.findIndex(group => group.letter == firstLetter)
if (groupIndex != -1) {
contacts.value[groupIndex].contacts.push({ "name": newContact.value })
} else {
const newGroup = {
"letter": firstLetter,
"contacts": [
{ "name": newContact.value }
]
}
contacts.value.push(newGroup)
}
}
// TODO:END
// 添加完成清空联系人输入框
newContact.value = "";
};
// 返回应用所需的数据和方法
return {
contacts,
newContact,
sortedContacts,
addContact
};
}
};
// 创建并挂载 Vue 应用
Vue.createApp(app).mount("#app");
</script>
</body>
</html>
第九题 商品浏览足迹(25分)
window.onload = async ()=> {
const MockUrl =`./js/data.json`;// 请求地址
let data=[];// 存储请求后的数据
// TODO:待补充代码,目标 1
let res =await axios.get(MockUrl)
// await等待异步操作完成,返回res,相当于then中的res
console.log(res)
data = res.data
// TODO:END
// 请求完成后调用下面的代码
const newData = getData(data);
showData(newData);
};
/**
* 将同一天浏览的相同商品去重并作为数组返回
* @param {Array} defaultData json 文件中读取到的原始数据
* @returns 去重后的数据,数据结构与 defaultData 相同
*/
const removeDuplicates = defaultData => {
console.log(defaultData)
let newData = [];
// TODO:待补充代码
const map = new Map();
defaultData.forEach(item=>{
if(!map.has(item.id)){
newData.push(item)
map.set(item.id,1)
}
});
console.log(newData)
return newData;
}
/**
* 将去重后的数据根据字段 viewed_on(格式化为 YYYY-MM-DD) 降序排序
* @param {*} defaultData 去重后的数据
* @returns 根据字段 viewed_on(格式化为 YYYY-MM-DD) 降序排序
*/
const sortByDate = defaultData => {
let newData = [];
// TODO:待补充代码
newData.push(...defaultData)
newData.sort((a, b) => {
return (new Date(b.viewed_on)) - (new Date(a.viewed_on))
})
return newData;
}
/**
* 将去重排序后的所有商品数据,作为一个对象存储并返回,
* 该对象的所有 `key` 为浏览商品的当天日期(即,字段 viewed_on 格式化为 YYYY-MM-DD),
* `value` 为当天浏览的所有商品(以数组形式表现)。
* @param {Array} defaultData 重后的所有商品数据
* @returns
*/
const transformStructure = defaultData => {
let newData = {};
// TODO:待补充代码
const hashMap = new Map()
defaultData.forEach(item => {
const date = new Date(item.viewed_on)
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
let arr = hashMap.get(dateStr)
if (!arr) {
arr = []
hashMap.set(dateStr, arr)
}
arr.push(item)
})
hashMap.entries().forEach(entry => {
const [date, data] = entry
newData[date] = data
})
return newData;
}
第十题 NPM Download Simulator(25分)
......
2023年十四届省赛大学组真题(共10道题)
第一题 电影院排座位(5分)
/* TODO:待补充代码 */
.seat-area {
display: grid;
margin-top: 50px;
grid-template-rows: repeat(6, 1fr);
grid-template-columns: repeat(8, 1fr);
gap: 10px;
}
.seat-area .seat:nth-child(8n + 2) {
margin-right: 20px;
}
.seat-area .seat:nth-child(8n + 7) {
margin-left: 20px;
}
第二题 图片水印生成(5分)
/**
* 创建一个文字水印的div
* @param {string} text - 水印文字
* @param {string} color - 水印颜色
* @param {number} deg - 水印旋转角度
* @param {number} opacity - 水印透明度
* @param {number} count - 水印数量
*/
function createWatermark(text, color, deg, opacity, count) {
// 创建水印容器
const container = document.createElement("div");
container.className = "watermark";
console.log(text, color, deg, opacity, count);
// TODO: 根据输入参数创建文字水印
let template = `<span style="color:${color};transform: rotate(${deg}deg); opacity:${opacity}">${text}</span>`;
for (let i = 0; i < count; i++) {
container.innerHTML += template;
}
return container;
}
// 以下代码不需要修改
// 调用createWatermark方法,创建图片水印
const watermark = createWatermark("WaterMark", "white", 45, 0.5, 11);
// 将水印挂载到图片容器上
const container = document.querySelector(".container");
container.appendChild(watermark);
// 提供图片保存功能
const button = document.querySelector("button");
button.addEventListener("click", () => {
domtoimage.toJpeg(document.querySelector(".container")).then((dataUrl) => {
const link = document.createElement("a");
link.download = "image.jpeg";
link.href = dataUrl;
link.click();
});
});
第三题 收集帛书碎片(10分)
function collectPuzzle(...puzzles) {
// TODO:在这里写入具体的实现逻辑
// 对所有的拼图进行收集,获取不同拼图类型的结果,并返回
let result = puzzles.flat(Infinity);
return [...new Set(result)];
}
// 检测需要,请勿删除
module.exports = collectPuzzle;
第四题 自适应页面(10分)
@media (max-width: 800px) {
#tutorials .row {
grid-template-columns: 1fr;
}
#tutorials img {
margin: 0;
}
#tutorials .text .box {
margin-bottom: 15px;
margin-top: 20px;
}
label.icon-menu {
display: block;
color: white;
padding: 0 20px;
line-height: 54px;
}
.menu {
height: 54px;
margin-bottom: 25px;
}
.menu li {
display: flex;
flex-direction: column;
background-color: #252525;
}
.collapse {
display: none;
}
.menu:hover .collapse {
display: flex;
flex-direction: column;
}
.dropdown:hover ul {
display: contents;
}
.dropdown:hover ul li {
background-color: white;
}
}
第五题 全球新冠疫情数据统计(15分)
第六题 年度明星项目(15分)
// 保存翻译文件数据的变量
let translation = {};
// 保存所有数据的变量
let data = [];
let page = 0;
// 记录当前语言
let currLang = "zh-cn";
// TODO: 请在此补充代码实现项目数据文件和翻译数据文件的请求功能
window.onload = async () => {
const res = await fetch("./js/all-data.json");
data = await res.json();
const res2 = await fetch("./js/translation.json");
translation = await res2.json();
//目标二
data.slice(0, 15).map((val) => {
let obj = {
icon: val.icon,
description: currLang == "zh-cn" ? val.descriptionCN : val.descriptionEN,
name: val.name,
stars: val.stars,
tags: val.tags,
};
return $(".list > ul").append(createProjectItem(obj));
});
};
// TODO-END
// TODO: 请修改以下代码实现项目数据展示的功能
let loadMore = document.querySelector(".load-more");
loadMore.addEventListener("click", () => {
page += 1;
data.slice(15 * page, 15 * (page + 1)).map((val) => {
let obj = {
icon: val.icon,
description: currLang == "zh-cn" ? val.descriptionCN : val.descriptionEN,
name: val.name,
stars: val.stars,
tags: val.tags,
};
return $(".list > ul").append(createProjectItem(obj));
});
if (page == 3) {
loadMore.style.display = "none";
}
});
// TODO-END
// 用户点击切换语言的回调
$(".lang").click(() => {
// 切换页面文字的中英文
if (currLang === "en") {
$(".lang").text("English");
currLang = "zh-cn";
} else {
$(".lang").text("中文");
currLang = "en";
}
$("body")
.find("*")
.each(function () {
const text = $(this).text().trim();
if (translation[text]) {
$(this).text(translation[text]);
}
});
// TODO: 请在此补充代码实现项目描述的语言切换
const p = document.querySelectorAll("ul li p");
if (currLang == "en") {
p.forEach((item, index) => {
item.innerHTML = data[index].descriptionEN;
});
} else if (currLang == "zh-cn") {
p.forEach((item, index) => {
item.innerHTML = data[index].descriptionCN;
});
}
});
// 生成列表DOM元素的函数,将该元素的返回值append至列表中即可生成一行项目数据
/**
* @param {string} name - 项目名称
* @param {string} description - 项目描述
* @param {string[]} tags - 项目标签
* @param {number} stars - 项目star数量
* @param {string} icon - 项目icon路径
*/
function createProjectItem({ name, description, tags, stars, icon }) {
return `
<li class="item">
<img src="images/${icon}" alt="">
<div class="desc">
<h3>${name}</h3>
<p>${description}</p>
<ul class="labels">
${tags.map((tag) => `<li>${tag}</li>`).join("")}
</ul>
</div>
<div class="stars">
+${stars} 🌟
</div>
</li>
`;
}
第七题 视频弹幕(20分)
const bullets = [
"前方高能",
"原来如此",
"这么简单",
"学到了",
"学费了",
"666666",
"111111",
"workerman",
"学习了",
"别走,奋斗到天明",
];
/**
* @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
let left = getEleStyle(videoEle).width;
let top = getRandomNum(getEleStyle(videoEle).height);
spanEle.innerHTML = bulletConfig.value;
spanEle.style.left = left + "px";
spanEle.style.top = top + "px";
spanEle.style.color = `rgb(${getRandomNum(255)},${getRandomNum(
255
)},${getRandomNum(255)})`;
videoEle.appendChild(spanEle);
setInterval(() => {
// 向左移动距离为 bulletConfig.speed(弹幕配置对象)。
left -= bulletConfig.speed;
spanEle.style.left = left + "px";
if (getEleStyle(spanEle).right <= getEleStyle(videoEle).left) {
videoEle.removeChild(spanEle);
clearInterval(timer);
}
}, bulletConfig.time);
}
document.querySelector("#sendBulletBtn").addEventListener("click", () => {
// TODO:点击发送按钮,输入框中的文字出现在弹幕中
let intVal = document.querySelector("#bulletContent").value;
bulletConfig.value = intVal;
document.querySelector("#bulletContent").value = "";
renderBullet(bulletConfig, videoEle, (isCreate = true));
});
function getEleStyle(ele) {
// 获得元素的width,height,left,right,top,bottom
return ele.getBoundingClientRect();
}
function getRandomNum(end, start = 0) {
// 获得随机数,范围是 从start到 end
return Math.floor(start + Math.random() * (end - start + 1));
}
// 设置 index 是为了弹幕数组循环滚动
let index = 0;
const videoEle = document.querySelector("#video");
// 弹幕配置
const bulletConfig = {
isHide: false, // 是否隐藏
speed: 5, // 弹幕的移动距离
time: 50, // 弹幕每隔多少ms移动一次
value: "", // 弹幕的内容
};
let isPlay = false;
let timer; // 保存定时器
document.querySelector("#vd").addEventListener("play", () => {
// 监听视频播放事件,当视频播放时,每隔 1000s 加载一条弹幕
isPlay = true;
bulletConfig.value = bullets[index++];
renderBullet(bulletConfig, videoEle);
timer = setInterval(() => {
bulletConfig.value = bullets[index++];
renderBullet(bulletConfig, videoEle);
if (index >= bullets.length) {
index = 0;
}
}, 1000);
});
document.querySelector("#vd").addEventListener("pause", () => {
isPlay = false;
clearInterval(timer);
});
document.querySelector("#switchButton").addEventListener("change", (e) => {
if (e.target.checked) {
bulletConfig.isHide = false;
} else {
bulletConfig.isHide = true;
}
});
第八题 外卖给好评(20分)
第九题 Markdown文档解析(25分)
第十题 组课神器(25分)
/**
* @description 模拟 ajax 请求,拿到树型组件的数据 treeData
* @param {string} url 请求地址
* @param {string} method 请求方式,必填,默认为 get
* @param {string} data 请求体数据,可选参数
* @return {Array}
* */
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") {
let dataList = localStorage.getItem("data");
if (dataList) {
result = JSON.parse(dataList);
} else {
await axios.get(url).then((res) => {
result = res.data.data;
});
}
} else if (method == "post") {
let newData = JSON.stringify(data);
localStorage.setItem("data", newData);
}
return result;
}
/**
* @description 找到元素节点的父亲元素中类选择器中含有 tree-node 的元素节点
* @param {Element} node 传入的元素节点
* @return {Element} 得到的元素节点
*/
const getTreeNode = (node) => {
let curElement = node;
while (!curElement.classList.contains("tree-node")) {
if (curElement.classList.contains("tree")) {
break;
}
curElement = curElement.parentNode;
}
return curElement;
};
/**
* @description 根据 dragElementId, dropElementId 重新生成拖拽完成后的树型组件的数据 treeData
* @param {number} dragGrade 被拖拽的元素的等级,值为 dragElement data-grade属性对应的值
* @param {number} dragElementId 被拖拽的元素的id,值为当前数据对应在 treeData 中的id
* @param {number} dropGrade 放入的目标元素的等级,值为 dropElement data-grade属性对应的值
* @param {number} dropElementId 放入的目标元素的id,值为当前数据对应在 treeData 中的id
*/
function treeDataRefresh(
{ dragGrade, dragElementId },
{ dropGrade, dropElementId }
) {
// 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);
}
function getDragElement(data, id) {
for (const obj of flatObj(data)) {
if (obj.id == id) return obj;
}
}
function flatObj(data) {
return data.reduce((prev, cur) => {
prev = [...prev, cur];
if (cur?.children) prev = [...prev, ...flatObj(cur.children)];
return prev;
}, []);
}
/**
* @description 根据 treeData 的数据生成树型组件的模板字符串,在包含 .tree-node 的元素节点需要加上 data-grade=${index}表示菜单的层级 data-index="${id}" 表示菜单的唯一id
* @param {array} data treeData 数据
* @param {number} grade 菜单的层级
* @return 树型组件的模板字符串
*
* */
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;
}
let treeData; // 树型组件的数据 treeData
// 拖拽到目标元素放下后执行的函数
const dropHandler = (dragElement, dropElement) => {
let dragElementId = dragElement.dataset.index;
let dragGrade = dragElement.dataset.grade;
if (dropElement) {
let dropElementId = dropElement.dataset.index;
let dropGrade = dropElement.dataset.grade;
treeDataRefresh({ dragGrade, dragElementId }, { dropGrade, dropElementId });
document.querySelector(".tree").innerHTML = treeMenusRender(treeData);
document.querySelector("#test").innerText = treeData
? JSON.stringify(treeData)
: "";
ajax({ url: "./js/data.json", method: "post", data: treeData });
}
};
// 初始化
ajax({ url: "./js/data.json" }).then((res) => {
treeData = res;
document.querySelector("#test").innerText = treeData
? JSON.stringify(treeData)
: "";
let treeEle = document.querySelector(".tree");
treeEle.dataset.grade = 0;
let treeTemplate = treeMenusRender(treeData);
treeTemplate && (treeEle.innerHTML = treeTemplate);
const mDrag = new MDrag(".tree-node", dropHandler);
// 事件委托,按下小图标记录得到被拖拽的元素,该元素 class 包含 .tree-node
document.querySelector(".tree").addEventListener("mousedown", (e) => {
e.preventDefault();
if (
e.target.nodeName.toLowerCase() === "img" &&
e.target.classList.contains("point-svg")
) {
let dragElement = getTreeNode(e.target);
// MDrag类的drag方法实现拖拽效果
mDrag.drag(e, dragElement);
}
});
});
/**
* @description 实现拖拽功能的类,该类的功能为模拟 HTML5 drag 的功能
* 鼠标按下后,监听 document 的 mousemove 和 mouseup 事件
* 当开始拖拽一个元素后会在 body 内插入对应的克隆元素,并随着鼠标的移动而移动
* 鼠标抬起后,移除克隆元素和 mousemove 事件,如果到达目标触发传入的 dropHandler 方法
*/
class MDrag {
constructor(dropElementSelector, dropHandler) {
// 目标元素的选择器
this.dropElementSelector = dropElementSelector;
// 拖拽到目标元素放下后执行的函数
this.dropHandler = dropHandler;
// 保存所有的目标元素
this.dropBoundingClientRectArr = [];
// 被拖拽的元素
this._dragElement = null;
// 拖拽中移动的元素
this._dragElementClone = null;
// 目标元素
this._dropElement = null;
// 拖拽移动事件
this._dragMoveBind = null;
// 拖拽鼠标抬起事件
this._dragUpBind = null;
this.init();
}
init() {
const dropElements = document.querySelectorAll(this.dropElementSelector);
this.dropBoundingClientRectArr = Array.from(dropElements).map((el) => {
return { boundingClientRect: el.getBoundingClientRect(), el };
});
}
dragMove(e) {
const { pageX, pageY } = e;
this._dragElementClone.style.left = `${e.pageX}px`;
this._dragElementClone.style.top = `${e.pageY}px`;
this.setMouseOverElementStyle(pageX, pageY);
}
dragend(e) {
// 移动到目标元素后mouseup事件触发,删除 this._dragElementClone 元素和解除mousemove/mouseup事件
const { pageX, pageY } = e;
document.removeEventListener("mousemove", this._dragMoveBind);
document.removeEventListener("mouseup", this._dragUpBind);
if (
Array.from(document.body.children).indexOf(this._dragElementClone) != -1
) {
document.body.removeChild(this._dragElementClone);
}
this._dropElement = this.getActualDropElement(pageX, pageY);
this.drop();
}
drag(e, dragElement) {
this._dragElement = dragElement;
this._dragElementClone = dragElement.cloneNode(true);
this._dragElementClone.style.position = "absolute";
this._dragElementClone.style.left = `${e.pageX - 20}px`;
this._dragElementClone.style.top = `${e.pageY - 20}px`;
this._dragElementClone.style.opacity = 0.5;
this._dragElementClone.style.width = "800px";
document.body.appendChild(this._dragElementClone);
// 绑定mousemove和mouseup事件
this._dragMoveBind = this.dragMove.bind(this);
this._dragUpBind = this.dragend.bind(this);
document.addEventListener("mousemove", this._dragMoveBind);
document.addEventListener("mouseup", this._dragUpBind);
return this;
}
getActualDropElement(pageX, pageY) {
const dropAttributeArr = this.dropBoundingClientRectArr.filter(
(obj) =>
pageY >= obj.boundingClientRect.top &&
pageY <= obj.boundingClientRect.top + obj.boundingClientRect.height
);
if (dropAttributeArr.length == 1) {
return dropAttributeArr[0].el;
} else if (dropAttributeArr.length > 1) {
let temp = dropAttributeArr.reduce((prev, next) => {
if (
Math.abs(pageY - prev.boundingClientRect.top) <=
Math.abs(pageY - next.boundingClientRect.top)
) {
return prev;
} else {
return next;
}
});
return temp.el;
} else {
return null;
}
}
setMouseOverElementStyle(pageX, pageY) {
let mousemoveEle = this.getActualDropElement(pageX, pageY);
if (mousemoveEle) {
this.dropBoundingClientRectArr.forEach((obj) => {
obj.el.classList.contains("mouseover-active") &&
obj.el.classList.remove("mouseover-active");
});
mousemoveEle.classList.add("mouseover-active");
}
}
drop() {
this.dropHandler && this.dropHandler(this._dragElement, this._dropElement);
this.init();
}
}