1.创意广告牌
在"绮幻山谷"的历史和"梦幻海湾"的繁华交汇之处,一块创意广告牌傲然矗立。它以木质纹理的背景勾勒出古朴氛围,上方倾斜的牌子写着"绮幻山谷的风吹到了梦幻海湾",瞬间串联了过去与现在,历史与现实。这独特的设计将风景、时光和情感交织在城市的喧嚣中,为路人带来一份令人陶醉的艺术享受。
1.1 问题题目
这个题目不多说了,只要知道这些css应该都能写出来,不会的平时多查查文档就记住了。
完善 css/style.css
的 TODO 部分,完成以下目标:
- 设置
.billboard
元素的圆角为10px
,背景图片为images
文件夹下的woodiness.jpg
。 - 设置
.top-sign
元素上面两个角是圆角15px
,下面两个角是直角,元素 X 轴倾斜-20
度。
完成后效果如下:
1.2 题目分析
这个题目不多说了,只要知道这些css应该都能写出来,不会的平时多查查文档就记住了。
1.3 源代码
.billboard {
position: relative;
background-color: #8e6534;
color: #fff;
padding: 20px;
box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.3);
background-size: cover;
/* TODO:待补充代码 设置圆角 10px,背景图片为woodiness.jpg */
border-radius: 10px;
background: url('../images/woodiness.jpg');
}
.top-sign {
position: relative;
width: 200px;
height: 100px;
background-color: #a87f4a;
display: flex;
justify-content: center;
align-items: center;
font-size: 1rem;
/* TODO:待补充代码 上面两个角是圆角 15px,下面两个角是直角 元素 x 轴倾斜 20度*/
border-top-left-radius: 15px;
border-top-right-radius: 15px;
transform: skewX(-20deg);
}
2.原子化 CSS
原子化 CSS 是一种近期十分流行的 CSS 构建方式,而属性化(Attributify)的原子化 CSS 进一步的简化了 CSS 的编写。
例如,传统上我们实现 flex
需要在元素上创建 class 等标识,然后在 CSS 中通过选择器选择到该元素:
<style>
.box {
display: flex;
}
</style>
<div class="box"></div>
而通过属性化的原子化 CSS,我们仅需:
<div flex></div>
即可完成同样效果。
2.1 问题题目
本题代码中 div
的其中一个属性为: flex="~ col"
,其中 ~
代表 flex
本身,表示使用 flex
布局,而 col
代表让 flex
纵向布局。
- 请补充
css/style.css
中的 TODO 部分,实现相关功能,让div
完成所需布局。
完成后的界面如图所示:
2.2 题目分析
这个题目主要需要知道的是css的属性选择器,可以查文档看看就知道了,不会的话其实看看其他的css应该也能联想到哈。
2.3 源代码
/* TODO: 实现原子化 flex */
[flex="~ col"] {
display:flex;
flex-direction: column;
justify-content: space-between;
}
3.神秘咒语
你作为一位勇敢的冒险家,发现了这两把钥匙,但你需要通过向服务器发送请求获取钥匙对应的咒语部分。每次点击钥匙按钮,会发送请求并将咒语部分显示在页面上。然而,这个神秘的宝藏箱对安全性要求很高,所以你需要在请求中携带正确的令牌(Token)才能获取到咒语。
3.1 问题题目
完善 index.js
中的 TODO 部分,通过新增或者修改代码,完成以下目标:
- 点击钥匙 1 和钥匙 2 按钮时会通过
axios
发送请求,在发送请求时需要在请求头中添加Authorization
字段携带token
,token
的值为2b58f9a8-7d73-4a9c-b8a2-9f05d6e8e3c7
。
完成后效果如下所示:
3.2 问题分析
这个考的也就是请求头携带参数,还不会的得抓紧学一下XML,fetch,axios了,这个从蓝桥杯模拟题的趋势来看,考的很频繁,甚至后面的题目发个请求就有3,5分。
3.3 问题解答
// TODO:新增或者修改以下代码
key1Button.addEventListener('click', async () => {
// 从后台请求钥匙1的咒语部分
key1Button.disabled = true;
let {data} = await axios.get('/spellone', {
headers:{
Authorization: '2b58f9a8-7d73-4a9c-b8a2-9f05d6e8e3c7'
}
})
console.log(data);
spell1.innerHTML = data;
tryOpenTreasureBox();
});
key2Button.addEventListener('click', async () => {
// 从后台请求钥匙2的咒语部分
key2Button.disabled = true;
let {data} = await axios.get('/spelltwo', {
headers:{
Authorization: '2b58f9a8-7d73-4a9c-b8a2-9f05d6e8e3c7'
}
})
spell2.innerHTML = data;
tryOpenTreasureBox();
});
4.朋友圈
在我们使用朋友圈这类带文字编辑功能的应用时,通常需要内容临时保存的功能,这样下一次编辑才不用从头开始。
本题请实现一个能够实时保存用户输入内容的朋友圈发布页面。
4.1 问题题目
请在 index.js
文件中补全代码,具体需求如下:
- 请将
debounce
函数补充完整,实现一个延迟为delay
毫秒的防抖函数。 - 用户在输入框(id=text)输入文字时,将用户输入的内容存入localStorage中,缓存的key名称为savedText;页面加载时检查localStorage中是否有缓存文本数据,若有则将输入框(id=text)内容设置为相应的文本;当用户点击“发表”按钮(id=post)时,清空输入框(id=text)中的内容,并将localStorage内缓存的文本数据移除。
- 此阶段的页面效果可以查看
effect-1.gif
文件。
- 此阶段的页面效果可以查看
- 当输入框中没有文字时,将“发表”按钮(id=post)的disabled属性值设置为disabled;如果输入框中有文字则移除该属性。
- 注意:当用户点击“发表”按钮和初次进入页面时也会改变输入框的内容,此时也需要对按钮的情况作出判断。
- 页面最终效果可以查看
effect-2.gif
文件。
4.2 问题分析
其实这个还算挺简单的,就是一个非常常规的防抖,会的自然肯定是会的,不会的可以花几分钟时间看看视频学一下,很快的。
4.3 问题解答
实现防抖函数
// 防抖工具函数
/**
* @param {function} fn - 回调函数
* @param {string} delay - 函数执行延迟,单位为ms
*/
function debounce(fn, delay) {
let timer = null
return function () {
timer ? clearTimeout(timer) : ''
timer = setTimeout(() => {
fn()
}, delay);
}
}
将内容存储到localStorage中
// 当文本框输入内容改变时,动态地设置localStorage缓存,并根据有没有文本改变按钮状态
// 此处使用了防抖函数,避免太过频繁地更新缓存
document.getElementById("text").addEventListener(
"input",
debounce(function() {
// 提示正在保存中
document.getElementById("prompt").textContent = "正在保存中...";
// TODO: 请在此补充用户输入时设置缓存和调整按钮状态的代码
localStorage.setItem('savedText', document.getElementById("text").value)
document.getElementById("post").removeAttribute('disabled')
// TODO-END
// 过一段时间后提示保存完成,模拟上传数据至后台的效果
setTimeout(function() {
document.getElementById("prompt").textContent = "内容已保存";
}, 750);
}, 200)
);
这个就是对页面的一些动态展示了,需要理清页面渲染的流程是怎么样的,什么时候显示什么,什么情况下该显示什么,经过一些判断后添加必要的属性,类名等就行。
document.getElementById("post").addEventListener("click", function() {
const content = document.getElementById("text").value;
const element = createContent(content);
document.querySelector(".contents").appendChild(element);
document.getElementById("prompt").textContent = "";
// TODO: 请在此补充用户点击“发表”按钮时清空文本框和缓存的代码
document.getElementById("text").value = ''
localStorage.removeItem('savedText')
document.getElementById("post").setAttribute('disabled', 'disabled')
});
页面初次加载的缓存
document.addEventListener("DOMContentLoaded", function() {
// TODO: 请在此补充页面加载时缓存检查的代码
const text = localStorage.getItem('savedText')
if (text) {
document.getElementById("text").value = text
}else{
document.getElementById("post").setAttribute('disabled', 'disabled')
}
});
5.美食蛋白质揭秘
《美食蛋白质揭秘》带您深入了解不同食物的蛋白质占比。通过精心设计的饼图,揭示食物中蛋白质的奥秘,助您做出更明智的饮食选择。探索这个神奇的蛋白质世界,解开食物比例的秘密,为您的健康饮食之路指引方向!
5.1 问题题目
找到 index.html
中的 TODO 部分,完成以下目标:
- 在不使用任何第三方库的情况下完成数据请求,请求地址必须使用提供的变量
MockURL
,数据中name
表示食物名称,value
表示蛋白质含量,将数据正确渲染到.protein-container
中,使用.protein-item
元素渲染数据,元素中显示食物名称和蛋白质含量,正确渲染后的 DOM 如下所示:
<div class="protein-container">
<div class="protein-item">鸡胸肉 30</div>
<div class="protein-item">牛肉 26</div>
<!-- 省略代码...... -->
</div>
- 在请求完成后,调用
echartsInit
方法渲染图表,参数data
须是下面的数据结构图标才会正确渲染:
[
{ name: "表头", icon: "none" },
// 原有数据
{ value: 30, name: "鸡胸肉" },
// 省略 .......
];
完成后效果如下:
5.2 问题分析
这里的小坑就是拿到的数据渲染出来是不对的,要去除掉多余的数据
5.3 问题解答
const arr = ref([])
onMounted(() => {
fetchData()
})
async function fetchData() {
// TODO:待补充代码
const res = await fetch(`${MockURL}`)
const data = await res.json()
arr.value = data
echartsInit( [{ name: "表头", icon: "none" }, ...arr.value])
arr.value.shift(arr.value)
}
return { // 返回setup,这样模版才能访问到
arr,
echartsInit,
};
<div id="app">
<h2>不同食物的蛋白质占比</h2>
<div class="protein-container" >
<!-- TODO:待补充代码,渲染获取的数据 -->
<div class="protein-item" v-for="item in arr" :key="item.name">{{item.name}} {{item.value}}</div>
</div>
<div class="echarts" id="main"></div>
</div>
6.营业状态切换
营业状态切换是一个基于 Vue.js 的店铺管理功能,该功能允许用户通过简单的界面切换店铺的营业状态。页面上展示了当前店铺的状况,并根据状态显示相应的图片。用户可以通过点击开关按钮来切换店铺的营业状态,从而实现营业中和已打烊之间的切换。
6.1 问题题目
找到 index.html
文件中的 useToggle
函数,完善其中的 TODO 部分,完成以下目标:
- 这个
useToggle
函数用于创建一个可切换状态的逻辑,它接受一个初始状态state
作为参数,并返回一个包含状态(true
或false
)和切换状态函数的数组。
完成后效果如下:
6.2 问题分析
这个题目要实现切换状态,通过观察题目可以看到,需要返回的是一个Boolean值,这个是来确定页面该如何渲染的,第二个需要返回的是一个函数,用来切换状态的。
首先我们要想要的是,返回的值肯定需要是一个ref(),响应式的值,因为我们知道啊,在页面不刷新的情况下,ref(),响应式数据发生变化,页面也会发生变化的,那么想到这个就好办了。
6.3 问题解答
function useToggle(state) {
// TODO:待补充代码
const isWorking = ref(state)
const toggleWorking = ()=> {
isWorking.value = !isWorking.value
}
return [isWorking, toggleWorking]
}
7.小说阅读器
小蓝想自己开发一个简易版的小说阅读器,其功能包括两部分:
- 将指定格式的 .txt 文件读取成固定的 JSON 格式并存储在指定 .json 文件中。
- 然后再将该 .json 文件内容以小说的章节结构显示在网页中。
7.1 问题题目
使用 Node.js 的 fs 模块
进行文件读写操作,完成 index.js
中的 TODO 部分。实现对 txt 文件的小说数据进行处理,最终可以展示在 index.html
中。具体要求如下:
-
在
run/index.js
中,完成readFile(file)
和writeFile(file,data)
函数的编写,实现读取 txt 小说文件,处理数据后写入 JSON 文件中。要求如下:-
在
readFile
函数读取文件(文件路径使用参数file
,小说内容格式查看run/book.txt
),并处理成指定格式的 JSON 数据,存储在result
中返回(需要考虑去除首尾多余的空格)。格式如下:txt 中格式 解析为 json 中对应字段 《武魂大陆》 "name":"《武魂大陆》",
第一卷:初入武魂 { "isRoll": true, "title": "第一卷:初入武魂" },
第1章:意外觉醒 主人公林风在一次偶然的意外中觉醒了自己的武魂,他的武魂是一只神秘的火凤凰。 {"title": "第1章:意外觉醒","content": ["主人公林风在一次偶然的意外中觉醒了自己的武魂,他的武魂是一只神秘的火凤凰。", "第二段", "第三段"]},
-
具体 json 格式如下:
{ "name":"《武魂大陆》", "data": [ { "isRoll": true, "title": "第一卷:初入武魂" }, { "title": "第1章:意外觉醒", "content": [ "主人公林风在一次偶然的意外中觉醒了自己的武魂,他的武魂是一只神秘的火凤凰。", "这个突如其来的力量使他既困惑又兴奋,他意识到自己获得了一种独特而强大的力量。", "从那一刻起,他踏上了在武魂大陆的修炼之旅。", ... "他的修炼之旅仍在继续,他对未来充满了期待和决心。无论面对怎样的挑战和困难,林风都坚信自己的武魂将引领他走向辉煌的未来。第一章内容" ] }, { "title": "第2章:武魂学院", "content": [ "林风进入了武魂学院,这是他展示自己才华和实力的舞台。", ... ] } ... ] }
-
在
writeFile
函数中,实现将readFile
函数中读取出来的数据,即data
参数,写入file
参数的文件中。 -
读写文件格式统一为
UTF-8
(即,run/index.js
中定义好的变量options
)。以上功能实现完成后,在终端运行node run/index.js
命令后检验book.json
文件中的内容是否正确写入。
-
-
在
component/myComponent.vue
文件created
方法中,使用 axios 请求 run 文件夹下的book.json
文件,并渲染到界面中,即:book.json
数据中的name
字段为书名,对应响应数据bookName
;data
字段为书的章节列表与内容,对应响应数据chapters
(书名和章节列表及内容的 DOM 渲染本题已提供)。 -
在
component/myComponent.vue
文件next
方法中,实现章节的切换,即点击 上一章 ,参数value
为 -1 ,切换为上一章内容。点击 下一章 ,参数value
为 1 ,切换为下一章内容。需要考虑边缘情况,即:当前页面已经是第一章时,“上一章”按钮点击失效;当前页面为最后一章时,“下一章”按钮点击失效。效果如下图:
完成后在浏览器中查看 index.html
效果如下:
无
- 第1章
- 动图
无
7.2 问题分析
7.3 问题解答
const fs = require('fs')
let readFilePath = './run/book.txt'
let writeFilePath = './run/book.json'
let options = 'UTF-8'
//读取txt小说文件,并按对应数据格式返回。
const readFile = (file) => {
try {
let result = null
let arr = {
name: '',
data: []
}
let res = fs.readFileSync(file, { encoding: options })
res = res.split('------------\n\n')
for (let i = 0; i < res.length; i++) {
const lines = res[i]
.trim()
.split('\r\n')
.filter((item) => {
return item !== '' && item !== '------------' && item !== ' '
})
arr.name = lines[0].slice(0, 8)
lines.splice(0, 1)
let temp1 = []
let temp2 = []
for (let i = 0; i < lines.length; i++) {
if (lines[i].slice(0, 3) === '---') {
if (temp2.length > 0) {
temp2.forEach((item) => {
arr.data.push(item)
})
}
temp1.push({
isRoll: true,
title: lines[i]
.split('---'[1])
.filter((item) => item !== '')
.join('')
})
arr.data.push(temp1[0])
temp1 = []
temp2 = []
} else if (lines[i].slice(0, 1) === '第') {
temp2.push({ title: lines[i], content: [] })
} else {
temp2[temp2.length - 1].content.push(lines[i].trim())
}
}
if (temp2.length > 0) {
temp2.forEach((item) => {
arr.data.push(item)
})
}
}
result = arr
return JSON.stringify(result)
} catch (err) {
return null
}
}
//写入json文件中
const writeFile = (file, data) => {
try {
fs.writeFileSync(file, data, { encoding: options })
} catch (err) {
console.log(err)
}
}
// 执行读取文件
let data = readFile(readFilePath)
// console.log(data)
if (data != null) writeFile(writeFilePath, data)
module.exports = {
writeFile,
readFile
}
component组件
next(value) {
// TODO:待补充代码
this.activeChapter += value
if (this.activeChapter <= 0) {
this.activeChapter = 1
return
}
if (this.activeChapter >= this.chapters.length - 1) {
this.activeChapter = this.chapters.length - 1
return
}
//跳过卷
if (this.activeChapter % 11 === 0 && value == 1) {
this.activeChapter += 1
}
if (this.activeChapter % 11 === 0 && value == -1) {
this.activeChapter -= 1
}
},
},
//通过axios发起请求json数据,并渲染界面。
created() {
// TODO:待补充代码
axios.get('../run/book.json').then((res) => {
this.bookName = res.data.name
this.chapters = res.data.data
})
},
8.冰岛人
2018 年世界杯,冰岛队因 1:1 平了强大的阿根廷队而一战成名。好事者发现冰岛人的名字后面似乎都有个“松”(son),于是有网友科普如下:
“冰岛人沿用的是维京人古老的父系姓制,孩子的姓等于父亲的名加后缀,如果是儿子就加 sson,女儿则加 sdottir。维京人的后裔是可以通过姓的后缀判断性别的,其他人则是在姓的后面加 m 表示男性,加 f 表示女性。”
因为冰岛人口较少,为避免近亲繁衍,本地人交往前先用个 App 查一下两人祖宗若干代有无联系。本题就请你实现这个 App 的功能。
8.1 问题题目
请在 js/index.js
文件中补全函数 marry
中的代码,该函数接收三个参数,第一个参数 data
表示当地人口数据信息,第二、三个参数 name1
、name2
是将要查询能否通婚的两个人的名字。
冰岛人沿用的是维京人古老的父系姓制,孩子的姓等于父亲的名加后缀,如果是儿子就加 sson
,女儿则加 sdottir
。维京人的后裔是可以通过姓的后缀判断性别的,其他人则是在姓的后面加 m
表示男性,加 f
表示女性 。
marry
根据 data
中当地人口数据信息,返回一个字符串,返回字符串的规则如下:
- 若两人为同性,则返回字符串
Whatever
; - 若有一人不在名单内,则返回字符串
NA
; - 若两人为异性,且五代以内(默认自己为第一代,注意不包括第五代)无公共祖先,则返回字符串
Yes
; - 若两人为异性,但五代以内(默认自己为第一代,注意不包括第五代)有公共祖先,则返回字符串
No
。
在页面两个输入框 #name1
和 #name2
中 分别输入要查询的两人的名字,然后点击查询按钮( #btn
),在页面中会显示两人能否通婚的字符串。
输入的名字的格式为“名 姓”,即名字在前,姓氏在后,名字与姓氏之间用一个空格分开。其中姓氏不加后缀。
完成效果如下图所示:
当地人口信息的数据结构如下:
[
{
"givenName":"chris",
"familyName":"smithm"
},
{
"givenName": "mike",
"familyName": "jacksson"
},
{
"givenName": "jack",
"familyName": "chrissson"
},
//...
]
中:
givenName
是名字。familyName
是姓氏。- 数据保证每个人的
givenName
在数据中是唯一的。
说明:
以数据中全名为 mike jacksson
的人为例,mike
是他的名,jacksson
是他的姓,他的姓的后缀为 sson
,姓去掉后缀 sson
为 jack
,那么就证明名为 mike
的人是名为 jack
的人的儿子。在数据中,我们可以看到名为 jack
的人的全名是 jack chrissson
,那么名为 jack
的人就是名为 chris
的人的儿子。chris
的全名为 chris smithm
,其姓的后缀为 m
,并不是 sson
或 sdottir
,对于 chris
,并不能向上追溯他的父亲是谁了,那么 chris
就是他们这一代的祖宗,通过祖宗姓的后缀为 m
我们可以知道祖宗是男性。
这样就构成了一种关系:jack
是 mike
的父亲,chris
是 jack
的父亲,即 chris
是 jack
的爷爷,同时 chris
还是 jack
与 mike
这一代的祖宗。
可以参考下面图解:
这里给出几个测试用例供考生使用(注意:其中姓氏不加后缀):
- 分别输入
tracy tim
与james eric
,结果为Yes
。 - 分别输入
will robin
与tracy tim
,结果为No
。 - 分别输入
bob adam
与eric steve
,结果为Whatever
。 - 分别输入
x man
与april mikes
,结果为NA
。
8.2 问题分析
首先的话,我是打算先进行基本的判断,是否是不存在的人,是否是同性,然后直接输出结果
找出每个人的祖先数组,然后再进行判断,查看是否满足题目要求,然后返回对应的结果。
8.3 问题解答
/**
* @description 通过输入的两个人的姓名返回相应的字符串
* @param {array} data 当地的人口信息
* @param {string} name1 要查询的两人名字之一
* @param {string} name2 要查询的两人名字之一
* @return {string} 根据被查询两人的名字返回对应的字符串
* */
function marry(data, name1, name2) {
function getAncestors(person, data) {
let ancestors = []
//找出person的父亲或者母亲
while (1) {
data.forEach((item) => {
if (item.givenName === /(\w+)(sson|sdottir)$/gi.exec(person.familyName)[1]) {
ancestors.push(item)
}
})
//判断是不是最后一个人了,查询ancestors的最后一个元素的familyName的后缀既不是sson也不是sdottir
if (!ancestors[ancestors.length - 1].familyName.endsWith('sson') && !ancestors[ancestors.length - 1].familyName.endsWith('sdottir')) {
break
}
//将personIndex的数据改成最新的数据
person = ancestors[ancestors.length - 1]
}
if (ancestors.length >= 4) {
ancestors.pop()
}
return ancestors
}
// 获取个人信息
let person1 = data.find((person) => person.givenName === name1.split(' ')[0])
let person2 = data.find((person) => person.givenName === name2.split(' ')[0])
// 如果其中一个人不在名单内,则返回 NA
if (!person1 || !person2) {
return 'NA'
}
let sex1 = ''
let sex2 = ''
// 获取性别
if (person1.familyName.endsWith('sson')) {
sex1 = 'male'
} else if (person1.familyName.endsWith('sdottir')) {
sex1 = 'female'
}
if (person2.familyName.endsWith('sson')) {
sex2 = 'male'
} else if (person2.familyName.endsWith('sdottir')) {
sex2 = 'female'
}
// 如果两个人为同性,则返回 Whatever
if (sex1 === sex2) {
return 'Whatever'
}
// 获取两个人的祖先数组(循环)
let ancestors1 = getAncestors(person1, data)
let ancestors2 = getAncestors(person2, data)
console.log(ancestors1, ancestors2)
// 判断是否有公共祖先
let hasCommonAncestor = false
for (let ancestor1 of ancestors1) {
for (let ancestor2 of ancestors2) {
if (ancestor1 === ancestor2) {
hasCommonAncestor = true
break
}
}
}
// 如果有公共祖先,则返回 No;否则返回 Yes
if (hasCommonAncestor) {
return 'No'
} else {
return 'Yes'
}
}
module.exports = marry
9.这是一个”浏览器“
各种类型的浏览器都是一页一页标签页的效果,小蓝现在也想模拟实现一个这样的效果,请你帮助小蓝实现。
9.1 问题题目
完善 js/index.js
文件,找到其中的 TODO 部分,完成代码,达到以下目标:
-
补全
js/index.js
中的toggleTab
函数,实现当点击标签页时,标签页与其内容页变为选中状态(即标签页加上类名liactive
,内容页加上类名conactive
)。上述且后文描述中的标签页是指
.fisrstnav ul
下的每个li
标签,内容页是指.tabscon
下的每个section
标签。完成效果如下:
-
完善
js/index.js
中的editTab
函数,实现当双击标签页文字或者内容页文字时出现输入框,当输入框失焦时,原标签页文字或内容页文字替换为输入框中输入的值。上述且后文描述中的标签页文字是指类名为
content
的span
标签中的文本;内容页文字是指.tabscon
下的每个section
标签的文本;输入框是指双击后出现的input
标签。完成效果如下:
-
补全
js/index.js
中的addTab
函数,实现当点击.tabadd
时,页面添加新的标签页(即创建一个li
标签作为子元素插入到.firstnav ul
节点下)和内容页(即创建一个section
标签作为子元素插入到.tabscon
节点下),新标签页及其内容页默认是选中状态。标签页及其内容页内容替换规则:-
标签页的内容分别按照序号 “标签页1、标签页2、标签页3…” 依次递增。
-
内容页的内容分别按照序号 “标签页1的内容、标签页2的内容、标签页3的内容…” 依次递增。
.firstnav ul
的 DOM 结构为:<ul> <li> <span class="content">标签页1</span> <span class="iconfont icon-guanbi"> <span class="glyphicon glyphicon-remove"> </span> </span> </li> <li> <span class="content">标签页2</span> <span class="iconfont icon-guanbi"> <span class="glyphicon glyphicon-remove"> </span> </span> </li> <li> <span class="content">标签页3</span> <span class="iconfont icon-guanbi"> <span class="glyphicon glyphicon-remove"> </span> </span> </li> </ul>
.tabscon
的 DOM 结构为:<div class="tabscon"> <section>标签页1的内容</section> <section>标签页2的内容</section> <section>标签页3的内容</section> </div>
完成效果如下:
-
-
补全
js/index.js
中的removeTab
函数,实现当点击某个标签页的.icon-guanbi
时,该标签页及其内容页从页面中删除,所有标签页及其内容页的内容仍以目标 3 中的规则开始重新排,标签页的选中状态也随之改变。标签页状态改变规则:- 若删除的标签页是当前选中的标签页,且非最后一个,则该标签页临近的下一个标签页变为选中状态。
- 若删除的标签页是当前选中的标签页,且为最后一个,则该标签页临近的上一个标签页变为选中状态。
- 若删除的标签页不是当前选中的标签页,则标签页选中状态不变。
完成后效果如下:
9.2 问题分析
9.3 问题解答
"use strict";
class Tab {
// 构造方法
constructor(id) {
// 获取元素
this.main = document.querySelector(id);
this.add = this.main.querySelector(".tabadd");
this.ul = this.main.querySelector(".fisrstnav ul");
this.fsection = this.main.querySelector(".tabscon"); //content页面
this.init();
}
// 初始化
init() {
this.updateNode();
// init初始化操作让相关元素绑定事件
this.add.onclick = this.addTab.bind(this.add, this);
for (var i = 0; i < this.lis.length; i++) {
this.lis[i].index = i;
this.lis[i].onclick = this.toggleTab.bind(this.lis[i], this);
this.remove[i].onclick = this.removeTab.bind(this.remove[i], this);
this.spans[i].ondblclick = this.editTab;
this.sections[i].ondblclick = this.editTab;
}
}
// 更新所有的li和section
updateNode() {
this.lis = this.main.querySelectorAll("li");
this.remove = this.main.querySelectorAll(".icon-guanbi");
this.sections = this.main.querySelectorAll("section");
this.spans = this.main.querySelectorAll(".content");
}
// 1.切换功能
toggleTab(event) {
// TODO: 添加代码,点击标签页,切换到对应标签页的功能
var num = this.getAttribute("num");
if (num != null && !(this.classList.contains("liactive"))) return; //阻止冒泡
event.clearClass(); //this.clearClass不行 因为指向的是元素
this.className = 'liactive'
event.sections[this.index].className = 'conactive'
// TODO结束
}
// 2.清空所有标签页及其内容页类名
clearClass() {
for (var i = 0; i < this.lis.length; i++) {
this.lis[i].className = "";
this.sections[i].className = "";
}
}
// 3.添加标签页
addTab(event) {
// TODO:添加代码,当点击加号,添加新的标签页(对应的内容页也应一并添加)
event.clearClass(); //清除一下
let newTab = document.createElement('li');
let lengthTab = event.lis.length + 1
newTab.className = 'liactive'
newTab.innerHTML = `<span class="content">标签页${lengthTab}</span>
<span class="iconfont icon-guanbi">
<span class="glyphicon glyphicon-remove">
</span>
</span>`
event.ul.appendChild(newTab);
//添加section内容
let newTabScon = document.createElement('section');
newTabScon.className = 'conactive';
newTabScon.innerHTML = `标签页${lengthTab}的内容`;
event.fsection.appendChild(newTabScon);
event.updateNode(); //需要更新一下节点
event.init();
// TODO结束
}
// 4.删除功能
removeTab(event) {
let index = Array.from(event.remove).indexOf(this); // 获取点击的删除按钮在数组中的索引
var lis = document.getElementsByTagName("li");
for (let index = 0; index < lis.length; index++) {
if (lis[index] == this.parentElement) {
this.parentElement.setAttribute('num', index);
}
}
if (event.lis[index].classList.contains('liactive')) { // 如果要删除的标签页是当前选中的标签页
if (index < event.lis.length - 1) { // 不是最后一个标签页
event.lis[index + 1].click(); // 选中临近的下一个标签页
} else if (index > 0) { // 是最后一个标签页
event.lis[index - 1].click(); // 选中临近的上一个标签页
}
}
event.ul.removeChild(event.lis[index]); // 从页面中删除对应的标签页
event.fsection.removeChild(event.sections[index]); // 从页面中删除对应的内容页
event.updateNode(); // 更新节点
}
// 5.修改功能
editTab() {
var str = this.innerHTML;
window.getSelection
? window.getSelection().removeAllRanges()
: document.Selection.empty();
this.innerHTML = '<input type="text" />';
var input = this.children[0];
input.value = str;
input.select(); //让文本框里的文字处于选定状态
// TODO:实现双击修改内容,当文本框失焦时,把修改的值赋给被双击的对象,并作上已修改的标记
let li = this;
input.onblur = function () {
li.innerHTML = this.value
}
// TODO结束
}
}
var tab = new Tab("#tab");
10. 趣味加密解密
小蓝正在制作一个趣味加密解密在线工具网站,本题请你帮助他封装两个 JS 函数 encryption
和 decryption
,以实现明文的加密及密文的解密。
10.1 问题题目
请在 index.js
文件中补全 encryption
函数和 decryption
函数代码的 TODO 部分,完成加密解密功能,即:
在左侧的输入框中输入明文,点击 加密 按钮调用 encryption
函数后,对明文进行加密,并输出到右侧的输入框中,反之同理。
index.js
文件中的代码说明如下:
名称 | 类型 | 描述 |
---|---|---|
defaultCodes | 常量 | 默认密码表,当输入的密码表长度小于 2 时采用请勿进行修改 ,否则可能造成考生提交的函数与检测用例所采用的默认密码表不一致,导致检测无法通过 |
string2Unit8Array | 函数 | 接受一个类型为字符串的参数 str 能够返回 str 以 UTF8 格式解码后的 Unit8Array 数组 |
uint8Array2String | 函数 | 接受一个类型为 Array 数组的参数 arr 能够返回 arr 以 UTF8 格式编码之后的 String 字符串 |
encryption | 函数 | 接受两个参数,分别是明文字符串 plainText 和用户输入的密码表字符串 codes 函数应当返回加密之后的密文字符串 |
decryption | 函数 | 接受两个参数,分别是密文字符串 cipherText 和用户输入的密码表字符串 codes 函数应当返回解密之后的明文字符串 |
index.js
文件中 encryption
函数和 decryption
函数的补完要求如下:
- 函数无需进行dom操作,只需要返回明文或密文 。
- 函数应当对用户输入的密码表字符串
codes
进行去重后再使用 。 - 若去重后的
codes
长度小于 2 ,应当采用默认密码表defaultCodes
。
以下是 加密流程 ,解密流程请 自行逆推 :
序号 | 名称 | 说明 |
---|---|---|
1 | 字符解码 | 将输入的明文以 UTF8 编码格式解码为 Unit8Array 数组 |
2 | 检查密码表 | 若输入的密码表去重后的长度小于 2 ,则采用默认密码表 defaultCodes |
3 | 进制转换 | 按照采用的密码表长度确定进制,然后对 Unit8Array 数组中的字符编码进行进制转换若使用了长度为 n 的密码表( n为大于 1 的任意整数 ),则应当将编码转为 n 进制,使得编码符号能够和密码表中的字符 一一对应例如,若使用了长度为 2 的密码表 ,明文为 “1” ,则需要将 “1” 的 10 进制字符编码 “49” 转换为 2 进制 “110001”以此类推 |
4 | 确定单位长度 | 根据采用的密码表的长度,确定字符编码需要几个符号表示例如,若使用了长度为 2 的密码表,则单位长度应为 8 ,因为在 2 进制下 1 个字节 8 个比特,需要 8 个符号表示以此类推在解密时,检查密码表后,若遇到长度并非单位长度整数倍的密文,则应立即返回字符串 “ERROR” |
5 | 补齐编码 | 在不足单位长度的编码前 补齐 数字 0例如,若使用了长度为 2 的密码表,则单位长度应为 8 ,而字符 “1” 的 2 进制编码为 “110001” ,故补齐为 “00110001”以此类推 |
6 | 编码映射 | 将补齐之后的字符编码,按照密码表中每个字符的次序,替换数字为密码表中的字符 ,得到密码数组例如,使用了文本 “密码” 作为密码表,则应当将字符编码中的 “0” 映射为 字符 “密” ,将 “1” 映射为字符 “码”则明文 “12” 的编码数组 [00110001,00110010][00110001,00110010] 应当映射为密码数组 [“密密码码密密密码”,“密密码码密密码密”][“密密码码密密密码”,“密密码码密密码密”]以此类推 |
7 | 翻转密码并输出 | 将每个密码中的字符顺序进行翻转,拼接为密文后返回例如,若明文 “12” 的密码数组为 [“密密码码密密密码”,“密密码码密密码密”][“密密码码密密密码”,“密密码码密密码密”],则应当翻转为 [“码密密密码码密密”,“密码密密码码密密”][“码密密密码码密密”,“密码密密码码密密”] 后,再拼接为密文 “码密密密码码密密密码密密码码密密” 并输出以此类推 |
关于单位长度的详细说明:
对于二进制,每个位有两种状态(0 和 1),所以一个字节(8 位)可以表示 256 种不同的状态,因为 2 的 8 次方等于256。因此在 2 进制下,需要 8 个符号表示,即单位长度为 8。
然而,在三进制系统中,每个位有三种可能的状态,分别是 0 、1 和 2 。但要想表示够 256 种状态,需要多少位呢?让我们来计算:
3^5 = 243,不够 256。
3^6 = 729,超过 256。
因此,用三进制表示,至少需要 6 位,即在 3 进制下,需要 6 个符号表示,即单位长度为 6。
关于其它进制的单位长度,以此类推。
10.2 问题分析
10.3 问题解答
function encryption(plainText, codes) {
// 去重、检查密码表
codes = [...new Set(codes)].join("");
codes = codes.length < 2 ? defaultCodes : codes;
// 进制为密码表长度
const radix = codes.length;
// 通过进制计算单位长度
const unitLength = Math.ceil(8 / Math.log2(radix));
// 对明文编码得到8位无符号整型数组
const uint8Array = string2Unit8Array(plainText);
// 对编码后的8位无符号整型数组进行加密
return [...uint8Array].map((u) => {
// 转换成指定进制
const bits = converter(u, radix);
// 根据单位长度进行补零
while (bits.length < unitLength) bits.unshift(0);
// 对照密码表进行编码映射并反转取字符串
return bits.map((bit) => codes[bit]).reverse().join("");
}).join("");
function converter(decNumber, base) {
const stack = [];
while (decNumber > 0) {
stack.push(decNumber % base);
decNumber = Math.floor(decNumber / base);
}
return stack.reverse();
}
}
function decryption(cipherText, codes) {
// 去重、检查密码表
codes = [...new Set(codes)].join("");
codes = codes.length < 2 ? defaultCodes : codes;
// 进制为密码表长度
const radix = codes.length;
// 通过进制计算单位长度
const unitLength = Math.ceil(8 / Math.log2(radix));
// 若密文长度不是单位长度的倍数,则返回错误
if (cipherText.length % unitLength !== 0) return "ERROR";
// 按照单位长度对密文进行解码
const decodedArray = [];
for (let i = 0; i < cipherText.length; i += unitLength) {
const slice = cipherText.slice(i, i + unitLength);
const codeArray = [];
// 查找每一个密文符号对应的索引值
for (let j = 0; j < slice.length; j++) {
codeArray.push(codes.indexOf(slice[j]));
}
// 若解码结果长度超过单位长度,则移除多余的部分
while (codeArray.length > unitLength) codeArray.shift();
// 根据进制和解码出来的索引值转换成原始值(十进制):系数*进制^权重之和
decodedArray.push(
codeArray.reduce(
(total, value, index) => total + value * Math.pow(radix, index)
)
);
}
// 最后将解码后的数组实例化为Uint8Array(8 位无符号整型数组)并转换为字符串
return uint8Array2String(Uint8Array.from(decodedArray));
}