目录
第一题 电影院排座位(5分)
* {
box-sizing: border-box;
}
body {
background-color: #242333;
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.container {
perspective: 1000px;
width: 470px;
}
.screen {
background-color: #fff;
height: 70px;
width: 100%;
transform: rotateX(-45deg);
box-shadow: 0 3px 10px rgba(255, 255, 255, 0.7);
color: #242333;
text-align: center;
line-height: 70px;
font-size: 30px;
}
.seat {
background-color: #444451;
height: 40px;
width: 45px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
/* TODO:待补充代码 */
.seat-area {
display: grid;
margin-top: 50px;
grid-template-columns: repeat(8, 1fr);
gap: 10px;
}
.seat-area .seat:nth-child(8n + 3) {
/*
:nth-child(8n + 3):这是一个结构伪类选择器,用于根据元素在其父元素中的位置来选择元素。具体规则如下:n 是一个计数器,它的取值从 0 开始,依次为 0、1、2、3……
当 n = 0 时,8n + 3 的值为 3,这意味着会选择父元素下的第 3 个子元素,前提是这个子元素的 class 属性包含 seat。
当 n = 1 时,8n + 3 的值为 11,会选择父元素下的第 11 个子元素,同样要求该元素的 class 属性包含 seat。
以此类推,n 每增加 1,就会选择间隔为 8 的子元素(因为系数是 8),并且这些元素的 class 属性都要包含 seat。
*/
margin-left: 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();
});
});
//方法二
/**
* 创建一个文字水印的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";
// TODO: 根据输入参数创建文字水印
for (var i = 0; i < count; i++) {
let SPAN = document.createElement("span");
SPAN.innerText = text;
SPAN.style.color = color;
SPAN.style.transform = `rotate(${deg}deg)`; //rotate 是字符串需要``包裹起来,不能rotate(`${deg}deg`)
SPAN.style.opacity = opacity;
console.log(SPAN.style);
container.appendChild(SPAN);
}
return container;
}
第三题 收集帛书碎片(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分)
Vue2 不考
第六题 年度明星项目(15分)
// 保存翻译文件数据的变量
let translation = {};
// 记录当前语言
let currLang = "zh-cn";
//all-data.json 文件中以数组的形式存储了明星项目的数据
let data = [];
let page = 0;
// TODO: 请在此补充代码实现项目数据文件和翻译数据文件的请求功能
window.onload = async () => {
let res = await fetch("./js/all-data.json");
data = await res.json();
let res2 = await fetch("./js/translation.json");
translation = await res2.json();
//目标二
data.slice(0, 15).map((v) => {
//map映射数据到新的数组
let objItem = {
icon: v.icon,
description: v.descriptionCN,
name: v.name,
stars: v.stars,
tags: v.tags,
};
return $(".list > ul").append(createProjectItem(objItem));
});
};
// TODO-END
// TODO: 请修改以下代码实现项目数据展示的功能
let loadMore = document.querySelector(".load-more");
loadMore.addEventListener("click", () => {
page += 1; //页码,默认0
data.slice(15 * page, 15 * (page + 1)).map((v) => {
//map映射数据到新的数组
let objItem = {
icon: v.icon,
description: currLang == "zh-cn" ? v.descriptionCN : v.descriptionEN,
name: v.name,
stars: v.stars,
tags: v.tags,
};
console.log(page); //循环3次
return $(".list > ul").append(createProjectItem(objItem));
});
if (page === 3) loadMore.style.display = "none";
});
// 以下代码(13-23行)为 createProjectItem 函数使用示例
// Mock一个项目的数据
// const item = {
// icon: "bun.svg",
// description:
// "Incredibly fast JavaScript runtime, bundler, transpiler and package manager...",
// name: "Bun",
// stars: 37087,
// tags: ["runtime", "bun"],
// };
// // 添加至页面的项目列表中,查看页面可以看到有一行 bun 的项目数据
// $(".list > ul").append(createProjectItem(item));
// 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: 请在此补充代码实现项目描述的语言切换
let pList = document.querySelectorAll("ul li p");
const result = (pList, currLang) => {
pList.forEach((el, idx) => {
el.innerHTML =
currLang == "en" ? data[idx].descriptionEN : data[idx].descriptionCN;
});
};
if (currLang == "en") {
result(pList, currLang);
} else {
result(pList, currLang);
}
});
// 生成列表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;
}
});
第八题 ISBN 转换与生成(20分)
// 将用户输入的带分隔符的 isbn 字符串转换只有纯数字和大写 X 字母的字符串
// 入参 str 为转换为包含任意字符的字符串
/**
*
* @param {String} str
* @returns
*/
function getNumbers(str) {
// TODO: 待补充代码
//只有纯数字和大写 X 字母的字符串。
// 除了 \d|X 之外的都替换成空字符串
return str.replace(/[^\d|X]/g, "");
}
// 验证当前 ISBN10 字符串是否有效
// 入参 str 为待判断的只有纯数字和大写 X 字母的字符串
/**
*
* @param {String} str
*/
function validISBN10(str) {
// TODO: 待补充代码
//格式要求
if (!/^\d{9}[\d|X]$/.test(str)) return false;
//校验位计算
let newArr = str.slice(0, 9).split("");
let sum = 0,
checkNum = 0;
check = 0;
newArr.forEach((el, idx) => {
sum += el * (idx + 1);
});
checkNum = sum % 11;
check = checkNum == 10 ? "X" : checkNum;
//这段代码首先检查输入的字符串是否符合 ISBN - 10 的基本格式要求,然后计算前 9 位数字的加权和,最后根据加权和的余数与最后一位校验位进行比较,判断输入的 ISBN - 10 是否有效。
return check == str[str.length - 1];
}
// 将用户输入的 ISBN-10 字符串转化为 ISBN-13 字符串
// 入参 isbn 为有效的 ISBN-10 字符串
/**
*
* @param {String} isbn
*/
function ISBN10To13(isbn) {
// TODO: 待补充代码
let isbn13 = "978" + isbn.slice(0, 9);
let odd = 0,
even = 0;
check = 0;
//计算最后一位校验位,前12位中的奇数位(是奇数的下标)
isbn13
.split("")
.map(Number) // 回调自己
.forEach((el, idx) => {
if ((idx + 1) % 2 == 0) {
even += 3 * el;
} else {
odd += 1 * el;
}
});
check = 10 - ((even + odd) % 10);
return isbn13 + String(check);
}
// 测试用例
console.log(getNumbers("7-5600-3879-4")); // 7560038794
console.log(getNumbers("7 5600 3879 4")); // 7560038794
console.log(validISBN10("7560038794")); // true
console.log(validISBN10("7560038793")); // false
console.log(validISBN10("756003879")); // false
console.log(validISBN10("756003879004")); // false
console.log(ISBN10To13("7560038794")); // 9787560038797
console.log(ISBN10To13("3598215088")); // 9783598215087
第九题 Markdown文档解析(25分)
000000
第十题 组课神器(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();
}
}