1.相不相等
小蓝想要自己开发一套用于 JS 单元测试的基础 API,先从编写一个用于检验两个数据值是否相等的函数开始吧,但是此时的小蓝却犯了难,聪明的你快来帮帮他吧~
1.1 题目要求
请你编写一个名为 expectFn
的函数,用于帮助开发人员测试他们的代码。它可以通过参数 val
接受任何值,并返回一个对象,该对象包含下面两个函数:
toBe(val)
:接受另一个值并在两个值相等(===
)时返回true
。如果它们不相等,则返回 “Not Equal” 。notToBe(val)
:接受另一个值并在两个值不相等(!==
)时返回true
。如果它们相等,则返回 “Equal” 。
示例如下:
// 示例 1:
输入:console.log(expectFn(5).toBe(5))
输出:true
解释:5 === 5 因此该表达式返回 true。
// 示例 2:
输入:console.log(expectFn(5).toBe(null))
输出:"Not Equal"
解释:5 !== null 因此抛出错误 "Not Equal".
// 示例 3:
输入:console.log(expectFn(5).notToBe(5))
输出:"Equal"
解释:5 === 5 因此抛出错误 "Equal".
// 示例 4:
输入:console.log(expectFn(5).notToBe(null))
输出:true
解释:5 !== null 因此该表达式返回 true.
1.2 题目分析
题目要求写的很清楚了,按照其要求做判断条件即可
1.3 源代码
var expectFn = function(val) {
// TODO
return {
toBe: (value) => {
return value === val ? true : 'Not Equal'
},
notToBe: (value) => {
return value !== val ? true : 'Equal'
}
}
}
2.三行情书
小蓝准备向小红表白,于是他在网上下单了三行情书网页程序,作为店主的你,快来帮他完成基本结构吧!
2.1 题目要求
请完善 style.css
的 TODO 部分,具体要求如下:
- 让第一行标题即
.content span
标签中的文字单行显示,多余溢出显示省略号。 - 请使用
-webkit-box
语法使得下面的文字即.content p
标签里的内容显示三行,多余溢出显示省略号。
完成后,页面效果如下:
2.2 题目分析
这里主要是如果写过案例的话,应该就知道怎么写,需要注意的是span元素是单行元素,要想达到效果,需要将其变为块级元素。
2.3 源代码
span {
font-size: 20px;
color: #837362;
/* TODO:补充下面的代码 */
/* 单行显示,多余溢出省略号 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
/* 使得溢出部分显示省略号 */
}
p {
color: #837362;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
/* 显示的行数 */
overflow: hidden;
}
3.电影院在线订票
随着人们生活水平的日益提升,电影院成为了越来越多的人休闲娱乐,周末放松的好去处。最近又有众多大片在院线上线,正好有小蓝想看的影片。小蓝赶紧邀约朋友们在线上平台订票。
3.1 题目要求
请在 js/script.js
文件中补全代码,最终实现订票功能。具体需求如下:
- 实现异步数据读取和渲染功能,使用axios请求./data.json(必须使用该路径请求,否则可能会请求不到数据)的数据。
- 将电影名字
name
渲染到 id 为movie-name
的节点中。 - 将电影售价
price
渲染到 id 为movie-price
的节点中。 - 将座位信息
seats
渲染到 id 为seat-area
的节点中,二维数组中每一个子数组代表一行,0 代表没有被他人占位,1 代表已经被订购。
- 将电影名字
每一行生成的 DOM 节点格式如下:
<!-- 每一行节点都使用 row class 包裹 -->
<div class="row">
<!-- 每个座位都具有 seat 的 class -->
<div class="seat"></div>
<!-- 如果座位被占了,则相应设置 occupied class -->
<div class="seat occupied"></div>
<!-- ...... -->
</div>
完成后的页面布局效果如下:
- 实现座位选择和总价计算的功能:
- 被别人订过的座位不能再被选择。
- 座位被选中后,状态更新,为相应的座位添加选中样式(即
selected
),并更新已选择的座位数和总价。 - 自己所选择的座位可以被取消,并更新已选择的座位数和总价。
完成后的效果见文件夹下面的 gif 图,图片名称为 effect.gif(提示:可以通过 VS Code 或者浏览器预览 gif 图片):
3.2 题目分析
- 第一点是使用axios发送请求获取到数据,这个不会的可以参考一下axios官网的案例学习一下
- 第二个是根据拿到的数据渲染html的结构,这个的就是创建对应的html结构,最后添加到对应的节点。
- 第三个是实现作为选择的功能,实现思路就是给所有的座位都绑定一个点击事件,通过事件对象身上是否被选中来替换类名,然后就是渲染数量。
3.3 源代码
/* TODO:
1. 完成数据请求,生成电影名,价格以及座位情况
2. 绑定点击事件,实现订票功能
*/
let data = {
}
axios
.get('../data.json')
.then((res) => {
console.log(res)
data = res.data
movieNameNode.innerHTML = data.name
moviePriceNode.innerHTML = data.price
//创建节点渲染数据
data.seats.forEach((item) => {
let row = document.createElement('div')
row.className = 'row'
item.forEach((item) => {
let seat = document.createElement('div')
seat.className = 'seat'
row.appendChild(seat)
if (item) {
seat.classList.add('occupied')
}
})
seatAreaNode.appendChild(row)
})
})
.catch((err) => {
console.log(err)
})
// 获取座位区域节点
const seatAreaNode = document.getElementById('seat-area')
// 获取电影名节点
const movieNameNode = document.getElementById('movie-name')
// 获取电影票价节点
const moviePriceNode = document.getElementById('movie-price')
// 获取已订电影票数量节点
const count = document.getElementById('count')
// 获取已订电影票总价节点
const total = document.getElementById('total')
// 获取所有座位节点
const seatNodes = document.querySelectorAll('.seat')
// 初始化已选择座位数和总价
let selectedSeatsCount = 0
let totalPrice = 0
// 监听座位点击事件
seatAreaNode.addEventListener('click', (event) => {
const clickedSeat = event.target
// 检查是否点击的是座位
if (clickedSeat.classList.contains('seat') && !clickedSeat.classList.contains('occupied')) {
// 切换座位的选中状态
clickedSeat.classList.toggle('selected')
// 更新已选择座位数和总价
if (clickedSeat.classList.contains('selected')) {
selectedSeatsCount++
totalPrice += data.price
} else {
selectedSeatsCount--
totalPrice -= data.price
}
// 更新显示
updateDisplay()
}
})
// 更新显示函数
function updateDisplay() {
count.textContent = selectedSeatsCount
total.textContent = totalPrice
}
4.老虎坤
小蓝开发了一款老虎机游戏。用户点击开始按钮后,页面上的三列图片开始随机滚动。当最后停下来的三张图片相同时,用户即可获得奖励。让我们一起来帮助小蓝完善这个老虎机应用吧!
4.1 题目要求
找到 js/index.js
中的 GetResult
函数,完成此函数,实现以下目标:
点击开始后,可以通过 GetResult
的三个参数 r1
、r2
、r3
计算出滚动后每一列图片的停留位置。当最后停留的图片都相同时,意味着玩家中了大奖!文字框(class = textPanel
)显示“恭喜你,中奖了”,否则显示:“很遗憾,未中奖”。
参数介绍:r1
、r2
、r3
表示的是三列元素下的 li
的最后停留位置,分别对应第一列(id=sevenFirst
)、第二列(id=sevenSecond
)、第三列(id=sevenThird
)。以第一列为例,最终显示的元素是 sevenFirst
下的第 r
个 li
元素。请使用显示的 li
元素的 data-point
属性判断三张图片是否相同。当 data-point
属性对应的值相同时,说明这三张图片相同。
在完成之后,请点击“开始”按钮,以下是未中奖和中奖的效果:
4.2 题目分析
这一题很明确的考察点就是获取元素身上的属性,需要用到getAttribute()方法,通过获取到的属性对比是否一样,如果三个相同则中奖。
4.3 源代码
if (sevenFirst.children[r1 - 1].getAttribute('data-point') == sevenSecond.children[r2 - 1].getAttribute('data-point')
&& sevenFirst.children[r1 - 1].getAttribute('data-point') == sevenThird.children[r3 - 1].getAttribute('data-point')) {
textPanel.innerHTML = '恭喜你,中奖了'
} else {
textPanel.innerHTML = '很遗憾,未中奖'
}
5.星际通讯
为了解密外星人的密文消息,科学家们创建了一个名为"星际通讯翻译器"的程序。这个程序可以将外星人的密文翻译成人类可以理解的语言。翻译器使用了一个称为"密文编码表"的参考表,其中包含了一系列密文和对应的人类语言翻译。
5.1 题目要求
完善 index.js
中的 translate
函数,完善其中的 TODO 部分:
translate
函数接收一个字符串参数 alienMessage
,其中包含一系列外星人的密文。函数根据特定的翻译规则将密文翻译成人类语言,并返回翻译后的结果字符串。外星人密文翻译规则存放在 codonTable
变量中。
注意:翻译后的结果字符串之间不能有空格。
特殊条件:
- 密文如果为空,直接返回空字符串。
- 如果密文任意一处无法翻译或遇到找不到对应翻译的密文,则返回字符串
无效密语
。 - 如果密文中出现了特殊密文对应的翻译结果是
stop
,则停止翻译,返回之前已翻译的结果(不包括对应stop
的密文)
以下为提供部分测试用例,通过测试用例不代表通过全部测试,请确保代码的通用性:
测试用例 | 输入字符串 | 预期输出 |
---|---|---|
1 | IIXIIIXIV |
人类你好交个朋友 |
2 | VIIIIIXIV |
哈喽你好交个朋友 |
3 (只翻译 stop 之前的密语) | IIXIIIXXIXIV |
人类你好 |
4 (只翻译 stop 之前的密语) | IIXXXIIIIXIV |
人类 |
5 | IIXxIIIXIV |
无效密语 |
6 | ax4 |
无效密语 |
7 | '' |
‘’ |
5.2 题目分析
这个题目就是js的切割 遍历
5.3 源代码
// 密语规则
const codonTable = {
'IIX': '人类',
'VII': '哈喽',
'III': '你好',
'IXI': '赞',
'XVI': '嗨',
'CUV': '打击',
'XII': '夜晚',
'IVI': '我',
'XIC': '想',
'XIV': '交个朋友',
'VIX': '月亮',
'XCI': '代码',
'XIX': '祈福',
'XVI': '和',
'XXI': 'stop',
};
/**
* @param {string} alienMessage 外星人的密文
* @return {string} 翻译结果
*/
const translate = (alienMessage) => {
let str = ''
// TODO:待补充代码
if (!alienMessage) return str
const arr = []
for(let i = 0; i < alienMessage.length; i+=3){
arr.push(alienMessage.slice(i, i + 3))
}
for(let i =0; i < arr.length; i++) {
if(!codonTable[arr[i]]) {
str = '无效密语'
break
}
if( arr[i] === 'XXI' ) {
break
}
str += codonTable[arr[i]]
}
return str
}
// 请注意:以下代码用于检测,请勿删除。
try{
module.exports = translate;
}catch(e){
}
6.蓝桥杯排位
今年的蓝桥排位赛非常热闹,全国的大学生们争先恐后参加,人气满满!小蓝想知道每个省份的参赛热度,特意做出了全国备赛地图与 备赛荣耀战力榜数据可视化大屏。可是数据怎么也调不对,导致大屏显示数据有误,快来帮帮小蓝处理数据,让大屏正确的运行起来吧!
6.1 题目要求
请在 js/index.js
文件中补全代码,具体需求如下:
- 完成数据请求(数据来源
./mock/map.json
),map.json
中存放的数据为省市对应的学校数据,使用到的字段介绍如下:
参数 | 类型 | 说明 |
---|---|---|
name | string | 省/市名称 |
school_count | number | 加入备赛的学校数量 |
value | number | 热度值 |
school_power | object | 战力存放对象,其中 name 为学校名称,power 为战力值 |
- 根据请求的数据正确完成左侧热力地图。
- 右侧战力榜中柱形图中,根据
power
字段的值对所有学校进行排序,取出排在前 10 名的学校,从左到右降序排列。
- 完成后运行起来,效果如下:
6.2 题目分析
根据要求获取数据,然后替换options中的配置即可
6.3 源代码
const {
createApp, ref, onMounted } = Vue
const app = createApp({
setup() {
const chartsData = ref([])
onMounted(() => {
// TODO:待补充代码 请求数据,并正确渲染柱形图和地图
axios
.get('../mock/map.json')
.then((res) => {
chartsData.value = res.data
showChartBar()
showChinaMap()
})
.catch((err) => {
console.log(err)
})
})
// 展示柱状图
const showChartBar = () => {
const myChart = echarts.init(document.getElementById('chart'))
let data = chartsData.value.map((item, index) => {
return item.school_power
})
console.log(data)
let result = data.flat(1).sort((a, z) => {
return z.power - a.power
})
let arr = result.slice(0, 10)
let school = arr.map((item) => {
return item.name
})
let power = arr.map((item) => {
return item.power
})
console.log(school)
console.log(power)
// 指定配置和数据
const option = {
xAxis: {
type: 'category',
axisLabel: {
interval: 0, rotate: 40 },
// TODO:待修改 柱状图 x 轴数据 -> 前 10 学校名称
data: school
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
yAxis: {
type: 'value',
boundaryGap: [0, 0.01]
},
series: [
{
// TODO:待修改 柱状图 y 轴数据->学校战力值
data: power,
type: 'bar',
showBackground: true,
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)'
},
itemStyle: {
color: '#8c7ae6'
}
}
]
}
// 把配置给实例对象
myChart.setOption(option)
// 根据浏览器大小切换图表尺寸
window.addEventListener('resize', function () {
myChart.resize()
})
}
// 展示地图
const showChinaMap = () => {
const chinaMap = echarts.init(document.getElementById('chinaMap'))
// 进行相关配置
const mapOption = {
tooltip: [
{
backgroundColor: '#fff',
subtext: 'aaa',
borderColor: '#ccc',
padding: 15,
formatter: (params) => {
return params.name + '热度值:' + params.value + '<br>' + params.data.school_count + '所学校已加入备赛'
},
textStyle: {
fontSize: 18,
fontWeight: 'bold',
color: '#464646'
},
subtextStyle: {
fontSize: 12,
color: '#6E7079'
}
}
],
geo: {
// 这个是重点配置区
map: 'china', // 表示中国地图
label: {
normal: {
show: false // 是否显示对应地名
}
},
itemStyle: {
normal: {
borderColor: 'rgb(38,63,168)',
borderWidth: '0.4',
areaColor: '#fff'
},
emphasis: {
//鼠标移入的效果
areaColor: 'rgb(255,158,0)',
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowBlur: 20,
borderWidth: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
},
visualMap: {
show: true,
left: 'center',
top: 'bottom',
type: 'piecewise',
align: 'bottom',
orient: 'horizontal',
pieces: [
{
gte: 80000,
color: 'rgb(140,122,230)'
},
{
min: 50000,
max: 79999,
color: 'rgba(140,122,230,.8)'
},
{
min: 30000,
max: 49999,
color: 'rgba(140,122,230,.6)'
},
{
min: 10000,
max: 29999,
color: 'rgba(140,122,230,.4)'
},
{
min: 1,
max: 9999,
color: 'rgba(140,122,230,.2)'
}
],
textStyle: {
color: '#000',
fontSize: '11px'
}
},
series: [
{
type: 'map',
geoIndex: 0,
// TODO:待修改 地图对应数据
data: chartsData.value.map((item) => {
return {
name: item.name,
school_count: item.school_count,
value: item.value
}
})
}
]
}
// 把配置给实例对象
chinaMap.setOption(mapOption)
}
return {
chartsData,
showChartBar,
showChinaMap
}
}
})
app.mount('#app')
7.拼出一个未来
在这个任务中,你将进入拼图游戏的世界,一个迷人的益智娱乐领域。拼图游戏一直以来都是受欢迎的休闲选择,要求玩家通过调整碎片位置,将图像拼凑成完整画面。在这个编程挑战中,你将体验到拼图游戏的魅力,通过编写代码实现交互逻辑,使拼图块能够在虚拟环境中重新排列,恰如其分地还原图像。通过这个任务,你将不仅提升编程技能,还能够理解游戏开发中的交互原理,为玩家创造出一个有趣的、具有挑战性的游戏体验。
7.1 题目要求
完善 js/index.js
的 TODO 部分,实现以下目标:
- 将拖动的拼图块与目标拼图块的图片进行交换,这包括交换图片的
src
属性和data-id
属性。待补充代码的drop
函数中现有的两个变量解释如下:draggedPiece
:代表被拖动的拼图块的图片元素的父元素。this
:代表当前目标位置的拼图块的图片元素父元素。
拼图成功后的 DOM 如下,图片 src
、alt
、data-id
均按照 1-9 顺序排列
- 显示/隐藏成功消息:拼图成功则设置成功消息元素(
id=success-message
)的class
名为show
,否则该元素的class
名为hide
。(注意:成功消息元素同时有且只能有一个class
名)
完成后效果如下:
7.2 题目分析
7.3 源代码
// 声明一个数组,包含了所有的拼图块数据
var puzzlePieces = [
{
src: './images/img1.png', id: 1 },
{
src: './images/img2.png', id: 2 },
{
src: './images/img3.png', id: 3 },
{
src: './images/img4.png', id: 4 },
{
src: './images/img5.png', id: 5 },
{
src: './images/img6.png', id: 6 },
{
src: './images/img7.png', id: 7 },
{
src: './images/img8.png', id: 8 },
{
src: './images/img9.png', id: 9 }
]
// 定义一个打乱数组的函数
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[array[i], array[j]] = [array[j], array[i]]
}
return array
}
// 使用定义的函数打乱拼图块数组
puzzlePieces = shuffleArray(puzzlePieces)
// 获取拼图容器元素
var container = document.getElementById('puzzle-container')
// 遍历拼图块数据数组
puzzlePieces.forEach(function (pieceData) {
// 创建一个新的拼图块元素
var piece = document.createElement('div')
piece.classList.add('puzzle-piece')
piece.setAttribute('draggable', 'true')
// 创建一个新的图片元素
var image = document.createElement('img')
image.src = pieceData.src
image.dataset.id = pieceData.id
// 将图片元素添加到拼图块元素中
piece.appendChild(image)
// 将拼图块元素添加到父容器元素中
container.appendChild(piece)
})
// 获取所有的拼图块元素,并转换为数组
const puzzleArray = Array.from(document.querySelectorAll('.puzzle-piece'))
// 获取成功消息元素
const successMessage = document.getElementById('success-message')
// 为每个拼图块元素添加拖拽事件监听器
puzzleArray.forEach((piece) => {
piece.addEventListener('dragstart', dragStart)
piece.addEventListener('dragover', dragOver)
piece.addEventListener('drop', drop)
})
// 声明一个变量用来保存正在拖动的拼图块
let draggedPiece = null
// 定义开始拖动事件的处理函数
function dragStart(event) {
draggedPiece = this
event.dataTransfer.setData('text/plain', null)
}
// 定义在拖动过程中的处理函数,阻止默认行为
function dragOver(event) {
event.preventDefault()
}
// 定义拖放事件的处理函数
function drop(event) {
// 检查是否拖动的拼图块不是当前目标拼图块
// draggedPiece 被拖动的拼图块元素。this 目标位置的拼图块元素。
let num = 0
if (draggedPiece !== this) {
// TODO:待补充代码
// 交换图片的 src 属性和 data-id 属性
let tempSrc = draggedPiece.querySelector('img').src
let tempDataId = draggedPiece.querySelector('img').dataset.id
draggedPiece.querySelector('img').src = this.querySelector('img').src
draggedPiece.querySelector('img').dataset.id = this.querySelector('img').dataset.id
this.querySelector('img').src = tempSrc
this.querySelector('img').dataset.id = tempDataId
// 检查是否拼图成功
puzzleArray.forEach((item, index) => {
if (parseInt(item.children[0].getAttribute('data-id')) === index + 1) {
num++
}
})
if (num === 9) {
successMessage.classList.remove('hide')
successMessage.classList.add('show')
} else {
successMessage.classList.remove('show')
successMessage.classList.add('hide')
}
// 重置正在拖动的拼图块
draggedPiece = null
}
}
// 定义拖放事件的处理函数
function drop(event) {
// 检查是否拖动的拼图块不是当前目标拼图块
// draggedPiece 被拖动的拼图块元素。this 目标位置的拼图块元素。
if (draggedPiece !== this) {
// TODO:待补充代码
console.log(draggedPiece.children[0]);
console.log(this.children[0]);
let src = draggedPiece.children[0].src
draggedPiece.children[0].src = this.children[0].src
this.children[0].src = src
console.log(src);
let id = draggedPiece.children[0].dataset.id
draggedPiece.children[0].dataset.id = this.children[0].dataset.id
this.children[0].dataset.id = id
console.log(puzzleArray);
let flag = puzzleArray.every((item, index) => {
return index+1 == item.children[0].dataset.id
})
if(flag){
successMessage.classList.remove('hide')
successMessage.classList.add('show')
}else{
successMessage.classList.add('hide')
successMessage.classList.remove('show')
}
}
// 重置正在拖动的拼图块
draggedPiece = null;
}
8.超能英雄联盟
当虚拟与现实交织,地球陷入巨大危机。一群源自不同虚拟世界的超级英雄,突然现身现实。原本只在游戏和漫画中存在的英雄们,现在踏足真实世界。
在跨时空的能量风暴中,你获得神秘力量,可以召唤虚拟世界的英雄。面对现实威胁,你集结英雄加入队伍,赋予他们真实存在。科学家发明“英雄链接器”,你从列表中选择队员,将能力融入现实,共同保卫家园。团结合作,寻找修复虚拟世界之法,最终打败黑暗势力,恢复平衡。现在,请你集结一支强大的队伍,以应对地球危机!
8.1 题目要求
请在 js/store.js
与 components
文件夹下两个组件中补全代码,最终实现可以从英雄列表中选择英雄,建立一支属于自己的队伍的功能。
具体需求如下:
-
完成可选英雄的渲染
所有英雄的信息存放于
js/heroes.json
中,请在components/HeroList.js
组件中使用axios
发送请求(请求地址写死为./js/heroes.json
,以免影响检测通过)获取到英雄信息,将其保存至js/store.js
的heroes
数组中,并将所有英雄渲染在“可选英雄”列表中(每条英雄信息是一个li
标签,需要生成对应英雄数量的li
标签作为子元素插入到.hero-list ul
中)。对
js/heroes.json
中英雄信息的解释:js/heroes.json
中存放了每个英雄的信息,以下述信息为例,其中id
是序号,name
是英雄姓名,ability
是英雄的能力,strength
是英雄的强度。{ id: 1, name: "绯红女巫", ability: "混沌魔法", strength: 98 }
用于展示可选英雄的
li
标签的 DOM 结构如下:<li class="hero-item"> <span>name</span> <span>ability</span> <span>strength</span> <button>添加至队伍/已添加</button> </li>
最终完成效果如下:
-
实现“添加英雄”功能
在需求 1 实现的基础上,当点击可选英雄左侧的“添加至队伍”按钮时,该按钮文本变为“已添加”,按钮变为禁用状态(按钮的
disabled
属性变为false
),并且将当前被选择的英雄的信息添加至页面右侧“我的队伍”列表中(英雄信息是一个li
标签,生成对应结构的li
标签作为子元素插入到.team-list ul
中)。用于展示已选英雄的
li
标签的 DOM 结构如下:<li class="team-item"> <span>name</span> <span>strength</span> <button>移除</button> </li>
-
实现“移除英雄”功能
在需求 2 实现的基础上,当点击已选英雄左侧“移除”按钮(
.sort-button
)时,该英雄从“我的队伍”列表中被移除,“可选英雄”列表中该英雄左侧“已添加”按钮文本变回“添加至队伍”,且按钮变为可用状态(按钮的disabled
属性变为true
)。 -
实现“按实力排序”功能
在需求 2 实现的基础上,当点击“按实力排序”按钮(
.sort-button
)时,将“我的队伍”中的英雄按照英雄强度降序排列。 -
实现实时显示队伍战斗力功能
在需求 2 、3 实现的基础上,
.total-strength
中应实时显示当前队伍的战斗力,即“我的队伍”中所有英雄的强度之和。
最终完成效果如下:
说明:
- 页面中共有 3 个组件,分别是
app
根组件,hero
组件和team
组件,其中后两个组件一直作为app
根组件的子组件出现。而上文中对“可选英雄列表”和“我的队伍列表”的描述分别指hero
组件与team
组件。 - 游戏中的人物数据会有很多界面/组件共享,所以需要存储在
pinia
状态管理器中,减少了程序花销也避免了同步问题。
8.2 题目分析
8.3 源代码
HeroList:
// TODO:补全代码,实现目标效果
const HeroList = {
template: `
<div class="hero-list">
<h2>可选英雄</h2>
<ul>
<li class="hero-item" v-for="(item,index) in store.heroes" :key="item.id">
<span>{
{item.name}}</span>
<span>{
{item.ability}}</span>
<span>{
{item.strength}}</span>
<button @click=store.add(item.id) :disabled="item.btn">{
{ item.btn ? '已添加' : '添加至队伍' }}</button>
</li>
</ul>
</div>
`,
setup() {
//第一步获取数据
const store = useHeroStore()
axios
.get('./js/heroes.json')
.then((res) => {
store.heroes = res.data
})
.catch((err) => {
console.log(err)
})
return {
store
}
}
}
// TODOEnd
TeamList:
// TODO:补全代码,实现目标效果
const TeamList = {
template: `
<div class="team-list">
<h2>我的队伍</h2>
<ul>
<li class="team-item" v-for="(item,index) in store.team" :key="item.id">
<span>{
{item.name}}</span>
<span>{
{item.strength}}</span>
<button @click=store.removeHero(item.id)>移除</button>
</li>
</ul>
<button class="sort-button" @click=store.sort>按实力排序</button>
<p class="total-strength">当前队伍战斗力:{
{store.totalStrength}} </p>
</div>
`,
setup() {
const store = useHeroStore()
return {
store
}
}
}
// TODOEnd
store.js:
const {
defineStore } = Pinia
const {
ref } = Vue
const useHeroStore = defineStore('hero', {
state: () => ({
heroes: [], //英雄列表
team: [] // 队伍列表
}),
// TODO:补全代码,实现目标效果
getters: {
//计算出战力总和strength
totalStrength() {
return this.team.reduce((total, hero) => {
return total + hero.strength
}, 0)
}
},
actions: {
add(id) {
this.heroes[id - 1].btn = true
this.team.push(this.heroes[id - 1])
},
removeHero(id) {
this.heroes[id - 1].btn = false
//移出team中的元素
this.team = this.team.filter((hero) => hero.id !== id)
},
sort() {
//按照实力排序strength
this.team.sort((a, b) => {
return b.strength - a.strength
})
}
}
// TODOEnd
})
9.实时展示权限日志
在实际开发中,我们可能会遇到记录用户操作的场景,我们需要将用户的操作以日志的形式记录下来,便于进行数据监控。
本题通过 Node.js 、Vue 和 Axios 来帮助考生实现这个效果,需要在已提供的基础项目中使用上述知识点来完善代码,最终实现需求中的具体功能。
9.1 题目要求
请在 js/node.js
和 js/index.js
文件中补全 TODO
部分的代码,最终实现以下需求:
一、请在 js/node.js
文件中补全代码,完善服务器的相应业务功能。
代码中已经搭建好了一个本地服务器,端口固定为 8080 ,直接在终端运行下面的命令即可启动服务。
node js/node.js
服务器做出的响应需要调用已经实现的 send
方法,参数可参见注释,响应的状态码 code
值为 0
。如未调用或者参数传递有问题,会导致检测不通过。
在读取文件数据时,建议使用绝对路径读取,本题已经在基础项目中给出相应文件的绝对路径,考生自行判断使用绝对路径或相对路径读取文件,本题对路径没有严格要求,能正确读取文件数据即可。
目标 1: 处理服务器的 GET 请求,请求地址为 /users
。仅需考虑如何读取 data.json
文件中的用户权限数据。文件数据读取完毕后,请调用封装好的 send
方法将文件数据响应给客户端。
目标 2: 处理服务器的 PUT 请求,请求地址为 /editUser
。仅需考虑如何根据已经获取到的请求体 body
修改目录中 data.json
文件数据对应的用户权限,请严格按照 data.json
文件中的对象格式修改,如不按要求则本需求检测不通过。文件数据修改完毕后,需要调用已实现的 send
方法将最新的文件数据响应给客户端。
本需求的对象格式为:
{
id: string, //用户的id
name: string, //用户姓名
power: boolean //用户的登录权限 true(允许用户登录)||false(禁止用户登录)
}
目标 3: 处理服务器的 POST 请求,请求地址为 /logger
。需考虑如何将已经获取到的请求体 body
转化成满足要求的对象(见下方示例,id
属性值可以使用代码中提供的 getLoggerId
方法获取唯一 id
也可以自行给定,需要保证该属性值是唯一的)并在该对象转化成 JSON
格式的末尾添加换行符(如不添加换行符会导致检测不通过)然后添加到目录中 logger.json
文件数据的末尾。文件数据添加完毕后,请调用已实现的 send
方法将最新的一条日志数据响应给客户端。
本需求要求的对象格式如下:
{
id: string, //日志的id
msg: string, //日志信息(请求体数据)
time: string, //创建日志的当前时间
}
二、请在 js/index.js
文件中补全代码,页面结构已经给出,请参考 index.html
文件,考生仅需实现请求数据渲染页面的相应功能。
在 js/index.js
文件中已经实现了 parseRes
方法,该方法用于解析服务器响应的数据,参数为响应对象。如果不调用该方法进行解析响应数据,可能会导致检测不通过。
目标 4: 请在 js/index.js
文件的 setup
方法中补全代码,实现获取用户权限数据并重新渲染用户权限列表功能。
-
在页面挂载前,向服务器请求用户权限数据,请求地址为
/users
,请求方法为GET
,获取到响应后需调用已实现的parseRes
方法解析响应数据。 -
将解析后的结果赋值给对应的响应式数据
userList
,用户权限列表渲染效果如下:
目标 5: 请在 js/index.js
文件的 setup
方法中补全代码,实现点击复选框修改对应的用户权限并打印日志信息的功能。
-
处理代码中提供的 handleChange 事件,获取当前点击的复选框的自定义属性:
data-id
,通过id
查找对应的用户,发送请求,请求方法为PUT
,请求地址为/editUser
,请求体(类型是字符串)格式见下方示例。获取响应后调用已实现的parseRes
方法解析响应数据,重新为对应的响应式数据赋值,达到重新渲染用户权限列表的目的。 请求体格式为:{ data:{ power: boolean //当前用户的登录权限 值为true||false }, params: { id: string //当前用户的id } }
-
处理代码中提供的 handleChange 事件,发送请求,请求方法为
POST
,请求地址为/logger
,请求体(类型是字符串)格式见下方示例。获取响应后调用已实现的parseRes
方法解析响应数据,重新为对应的响应式数据赋值,达到重新渲染日志查看区域的目的。本需求还需将最新一条日志显示在该区域的最上面。 请求体格式为:{ data:`超级管理员将用户${ 用户名}设置为${ getPowerText(用户修改后的权限)}权限` }
实现效果如下:
9.2 题目分析
9.3 源代码
node.js:
/**
* 请完成下面的 TODO 部分,其他代码请勿改动
*/
const fs = require('fs')
const http = require('http')
const path = require('path')
const dataUrl = path.resolve(__dirname, '../data.json')
const loggerUrl = path.resolve(__dirname, '../logger.json')
// 获取唯一的id
function getLoggerId() {
return Buffer.from(Date.now().toString()).toString('base64') + Math.random().toString(36).substring(2)
}
/**
* 该方法统一了服务器返回的消息格式,并返回给客户端
* @param {*} res 响应 response
* @param {*} code 状态码,默认为 0 代表没有错误,如果有错误固定为 404
* @param {*} msg 错误消息,固定为空字符串即可 ''
* @param {*} data 响应体,为 js 对象,若 data 为 utf-8 编码时需要使用 eval(data) 处理
*/
function send(res, code, msg, data) {
const responseObj = {
code,
msg,
data
}
const da = JSON.stringify(responseObj)
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.write(da)
res.end()
}
function handleStatic(res, pathName, part) {
const content = fs.readFileSync(path.resolve(__dirname, pathName))
let contentType = 'text/html'
switch (part) {
case 'css':
contentType = 'text/css'
break
case 'js':
contentType = 'text/js'
break
}
res.writeHead(200, 'Content-Type', contentType)
res.write(content)
res.end()
}
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
if (req.url === '/') {
handleStatic(res, '../index.html', '')
} else if (req.url === '/css/index.css') {
handleStatic(res, `..${
req.url}`, 'css')
} else if (req.url === '/js/index.js') {
handleStatic(res, `..${
req.url}`, 'js')
} else if (req.url === '/js/axios.min.js') {
handleStatic(res, `..${
req.url}`, 'js')
} else if (req.url === '/js/vue3.global.min.js') {
handleStatic(res, `..${
req.url}`, 'js')
}
if (req.method === 'GET' && req.url === '/users') {
// TODO 处理获取文件内容的操作
//读取data.json中的数据
let fileContent = fs.readFileSync(dataUrl, 'utf-8')
let data = JSON.parse(fileContent)
if (fileContent) {
//将读取到的数据转化为json格式
//将json格式的数据响应给客户端
send(res, 0, '', data)
}
} else if (req.method === 'PUT' && req.url === '/editUser') {
let fileContent = fs.readFileSync(dataUrl, 'utf-8')
let data = JSON.parse(fileContent)
let body = ''
req.on('readable', () => {
let chunk = ''
if (null !== (chunk = req.read())) {
body += chunk
}
})
req.on('end', () => {
if (body) {
// TODO 处理更改文件数据并将最新的文件数据响应给客户端
//处理put请求
let bodyData = JSON.parse(body)
//修改data.json中的数据
data.forEach((item) => {
if (item.id == bodyData.id) {
item.power = bodyData.power
}
})
//存储文件数据到data.json中
fs.writeFileSync(dataUrl, JSON.stringify(data))
send(res, 0, '', data)
}
})
} else if (req.method === 'POST' && req.url === '/logger') {
let body = ''
req.on('readable', () => {
let chunk = ''
if (null !== (chunk = req.read())) {
body += chunk
}
})
req.on('end', () => {
let fileContentLog = fs.readFileSync(loggerUrl, 'utf-8')
//判断是否有日志
let dataLog = []
if (fileContentLog) {
dataLog = JSON.parse(fileContentLog)
}
let fileContentUser = fs.readFileSync(dataUrl, 'utf-8')
let dataUser = JSON.parse(fileContentUser)
if (body) {
// TODO 处理新增日志
let bodyData = JSON.parse(body)
let dataJson = {
id: getLoggerId(),
msg: bodyData.data,
// 时间格式为:2023/6/6 上午8:10:35
time: `${
getTime()}`
}
//存储日志
dataLog.unshift(dataJson)
// 并在该对象转化成 JSON 格式的末尾添加换行符(如不添加换行符会导致检测不通过)
fs.writeFileSync(loggerUrl, JSON.stringify(dataLog, null, 2) + '\n')
send(res, 0, '', dataJson)
}
})
}
})
function getTime() {
// 获取当前时间
const currentDate = new Date()
// 获取年、月、日、时、分、秒
const year = currentDate.getFullYear()
const month = currentDate.getMonth() + 1 // 月份是从 0 开始的,所以要加 1
const day = currentDate.getDate()
const hours = currentDate.getHours()
//获取是上午还是下午
const amPm = hours >= 12 ? '下午' : '上午'
const minutes = currentDate.getMinutes()
const seconds = currentDate.getSeconds()
// 格式化时间
const formattedTime = `${
year}/${
month}/${
day} ${
amPm}${
hours}:${
minutes}:${
seconds}`
return formattedTime
}
server.listen(8080, () => {
console.log('Server running on port 8080')
})
index.js:
/**
* 请完成下面的 TODO 部分,其他代码请勿改动
*/
// 对响应进行统一处理,如果不调用该函数,可能导致判题出错
// 参数为服务器的响应对象
function parseRes(res) {
return (res.json && res.json()) || res.data
}
const App = {
setup() {
const {
onMounted } = Vue
const data = Vue.reactive({
userList: [], //用户数组
loggerList: [] //日志数组
})
const getPowerText = (power) => {
return power ? '可以登录' : '禁止登录'
}
const handleChange = async (e) => {
if (e.target.tagName !== 'INPUT') {
return
}
// TODO 处理发送请求修改当前用户的权限并更新一条日志记录
//处理put请求
let res = await axios.put(`/editUser`, {
id: e.target.dataset.id,
power: e.target.checked
})
if (res.status == 200) {
data.userList = parseRes(res.data)
} else {
console.log('修改失败')
}
//调用post请求,添加一条修改日志
//用id找出用户名
let userName = data.userList.find((item) => item.id == e.target.dataset.id).name
let postRes = await axios.post('/logger', {
data: `超级管理员将用户${
userName}设置为${
getPowerText(e.target.checked)}权限`
})
if (postRes.status == 200) {
//将数据放在数组首
let a = parseRes(postRes.data)
data.loggerList.unshift(a)
} else {
console.log('添加日志失败')
}
}
// TODO 在页面挂载之前请求用户数据并修改对应的响应数据
//利用axios获取数据
const getUserData = async () => {
let res = await axios.get('/users')
if (res.status == 200) {
data.userList = res.data.data
} else {
getUserData()
}
}
onMounted(() => {
getUserData()
})
return {
data,
handleChange,
getPowerText,
getUserData
}
}
}
const app = Vue.createApp(App)
app.mount(document.querySelector('#app'))
10.账户验证
生活中各种平台的登录方式,除了账号密码登陆外,也有使用手机号登录的选项。现在小蓝想实现一个使用手机号验证码登录的场景,但是小蓝由于学艺不精,关键部分功能不知道如何实现,请你帮助他完成。
10.1 题目要求
请在 index.html
文件中补全代码,最终实现使用手机号发送验证码验证登录的效果。
具体需求如下:
-
实现发送验证码功能
在
phone
组件中,当点击下一步按钮时(#btn
),对输入框(#numberInput
)中输入的手机号码的有效性以及是否同意下方协议(复选框#checkbox
是否是选中状态)做检验。当且仅当上述两条件均满足时,在网页右上角弹出成功提示框,标题为“发送成功”,提示内容为“您的验证码为XXXXXX”(XXXXXX是生成的验证码),且组件由phone
组件跳转至check
组件;否则弹出失败提示框,标题为“发送失败”,提示内容为“无效的手机号码”或“请先阅读并同意下方协议”,若都不满足,则提示内容为“请先阅读并同意下方协议”。发送验证码成功后,在
check
组件中的.hassend i
标签中显示经过处理的的手机号码(保留前三位和后两位,其余位替换为字符*
)。
说明:
- 若手机号码是以 18 开头的 11 位数字,则视为有效,否则视为无效。
- 验证码为随机生成的 6 位数字,由考生自行生成。
- 注意提示内容必须为上述字眼,不能有错别字或空格,否则影响判分结果。
最终完成效果如下:
-
实现输入验证码验证的功能
发送验证码成功后,在
check
组件的#code-container
中有 6 个input
标签,分别用于输入验证码的每一位,第一个input
默认聚焦。在向验证码输入框中输入内容时,鼠标焦点按照下述规则跳转:- 除了最后一个
input
标签,在任意input
标签中输入数字后,鼠标焦点会自动跳转至当前input
标签的下一个input
标签。 - 除了第一个
input
标签,在任意input
标签中删除数字时,鼠标焦点会自动跳转至当前input
标签的上一个input
标签。
当每个
input
标签均输入过内容时会自动验证输入的验证码是否正确,如果正确,则在页面右上角弹出成功提示框,标题为“验证成功”,提示内容为“欢迎回来”,且组件由check
组件跳转至success
组件;如果不正确,则清空 6 个input
输入框中的值,在右上角弹出失败提示框,标题为“验证失败”,提示内容为“您输入的验证码有误”,点击重新发送(
#resend
)会重新发送验证码,生成的验证码规则及提示内容同需求 1 。 - 除了最后一个
最终完成效果如下:
说明:
- 题中提供了
pinia
状态管理库,考生自行选择是否使用。 - 若多次生成验证码,则以最后一次生成的验证码为准。
- 如果本题使用到了定时器,则定时器的等待时间不应超过 20ms ,以免影响检测通过。
- 页面中共有四个组件,分别是
app
根组件,phone
组件,check
组件,success
组件。在页面使用过程中,后三个组件总会作为app
根组件的子组件出现,请使用<component></component>
标签实现后三个组件的跳转切换效果。 - 本题在右上角弹出的成功或失败提示框是使用
element-plus
中的Notification 通知
实现的,其 API 如下:
名称 | 说明 | 类型 | 默认 |
---|---|---|---|
title | 标题 | string | “” |
message | 提示内容 | string | “” |
type | 提示类型 | enum( “success” 、“warning”、“info”、“error”、“”) | “” |
position | 弹出位置 | enum( “top-right” 、“top-left”、“bottom-right”、“bottom-left”) | “top-right” |
duration | 显示时间,单位为毫秒,值为 0 则不会自动关闭 | number | 4500 |
使用示例:
<template>
<el-button plain @click="open1"> Success </el-button>
<el-button plain @click="open2"> Error </el-button>
</template>
<script lang="ts" setup>
import { ElNotification } from 'element-plus'
const open1 = () => {
ElNotification({
title: 'Success',
message: 'This is a success message',
type: 'success',
})
}
const open2 = () => {
ElNotification({
title: 'Error',
message: 'This is an error message',
type: 'error',
})
}
</script>
10.2 题目分析
10.3 源代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>账户验证</title>
<link rel="stylesheet" type="text/css" href="./css/index.css" />
<link rel="stylesheet" href="./css/[email protected]/index.css">
<script src="./js/vue3.global.js"></script>
<script src="./css/[email protected]/index.full.js"></script>
<script type="importmap">
{
"imports": {
"vue-demi": "./js/index.mjs",
"vue": "./js/vue.esm-browser.prod.js",
"pinia": "./js/pinia.esm-browser.js"
}
}
</script>
<script src="./js/pinia.esm-browser.js" type="module"></script>
</head>
<body>
<!-- app根组件开始 -->
<div id="app">
<div class="header">
<img class="back-btn" src="images/arrow.png" />
<span id="main_title">使用手机号登录</span>
<span class="blank"></span>
</div>
<component :is="showName"></component>
</div>
<!-- app根组件结束 -->
<!-- phone组件开始 -->
<template id="phone">
<div>
<ul class="phone">
<span>输入手机号码</span>
<li>
<input v-model="phoneVal" type="text" autofocus id="numberInput" />
</li>
<li>
<input v-model="isSure" type="checkbox" name="" id="checkbox" />
<span>已阅读并同意
<a href="javascript:;">服务协议</a>
和
<a href="javascript:;">隐私保护指引</a>
</span>
</li>
<button id="btn" @click="nextStep">下一步</button>
</ul>
</div>
</template>
<!-- phone组件结束 -->
<!-- check组件开始 -->
<template id="check">
<ul class="number">
<span>输入短信验证码</span>
<li class="hassend">已向
<i>{
{ handlePhoneVal }}</i>
发送验证码
</li>
<li class="code-container">
<input v-for="(item, index) in verificationCodeInput" :key="index" v-model="item" @input="handleInput(index)"
@keydown="handleKeyDown(index)" class="code" type="number" min="0" max="9" ref="codeInput{
{index}}"
required />
</li>
<a href="javascript:;" id="resend" @click="resendCode">重新发送</a>
</ul>
</template>
<!-- check组件结束 -->
<!-- success组件开始 -->
<template id="success">
<div class="success">
<ul>
<div>验证成功!</div>
<div>5s后将自动跳转</div>
</ul>
</div>
</template>
<!-- success组件结束 -->
</body>
<script type="module">
import {
createPinia } from 'pinia';
import {
createApp, ref, reactive, provide, inject, onBeforeMount } from 'vue';
const {
ElNotification } = ElementPlus;
const app = createApp({
setup() {
const data = reactive({
showName: 'phone',
});
const code = ref([]);
const phoneVal = ref('');
const createCode = function () {
let res = '';
function* _create() {
let count = 0;
while (++count <= 6) {
yield Math.floor(Math.random() * 10);
}
}
for (const iterator of _create()) {
res += iterator;
}
return res;
};
const handlePhone = (num) => {
let res = '';
for (let idx in num) {
if (idx > 2 && idx < num.length - 2) {
res += '*';
} else {
res += num[idx];
}
}
return res;
};
provide('code', code);
provide('phoneVal', phoneVal);
provide('createCode', createCode);
provide('data', data);
provide('handlePhone', handlePhone);
return {
...data,
};
},
});
app.use(ElementPlus);
app.use(createPinia());
app.component('phone', {
template: '#phone',
setup() {
const isSure = ref('');
const phoneVal = inject('phoneVal');
const code = inject('code');
const createCode = inject('createCode');
const data = inject('data');
function verifyPhone(num) {
if (num.length !== 11) return false;
return num[0] === '1' && num[1] === '8';
}
return {
isSure,
phoneVal,
nextStep() {
if (!isSure.value)
return ElNotification({
title: '发送失败',
message: '请先阅读并同意下方协议',
type: 'error',
});
if (!verifyPhone(phoneVal.value))
return ElNotification({
title: '发送失败',
message: '无效的手机号码',
type: 'error',
});
code.value = createCode();
ElNotification({
title: '发送成功',
message: '您在验证码为' + code.value,
type: 'success',
});
data.showName = 'check';
},
};
},
});
app.component('check', {
template: '#check',
setup() {
const phoneVal = inject('phoneVal');
const handlePhoneVal = inject('handlePhone')(phoneVal.value);
const data = inject('data');
const code = inject('code');
const createCode = inject('createCode');
const verificationCodeInput = Array(6).fill('');
onBeforeMount(() => {
setTimeout(() => {
const oCodeIptList = [...document.getElementsByClassName('code')];
oCodeIptList[0].focus();
oCodeIptList.map((item) => {
item.oninput = function () {
if (item.value) {
item?.nextElementSibling && item?.nextElementSibling.focus();
} else {
item?.previousElementSibling && item?.previousElementSibling.focus();
}
trackVal();
};
});
function trackVal() {
const val = verificationCodeInput.join('');
if (val.length === 6) {
if (val === code.value) {
ElNotification({
title: '验证成功',
message: '欢迎回来',
type: 'success',
});
data.showName = 'success';
} else {
ElNotification({
title: '验证失败',
message: '您输入的验证码有误',
type: 'error',
});
verificationCodeInput.fill('');
oCodeIptList[0].focus();
}
}
}
});
});
return {
handlePhoneVal,
verificationCodeInput,
handleInput(index) {
if (index < 5 && verificationCodeInput[index].length === 1) {
this.$refs[`codeInput${
index + 1}`]?.focus();
} else if (index > 0 && verificationCodeInput[index].length === 0) {
this.$refs[`codeInput${
index - 1}`]?.focus();
}
trackVal();
},
handleKeyDown(index) {
if (event.key === 'Backspace' && index > 0) {
this.$refs[`codeInput${
index - 1}`]?.focus();
}
},
trackVal() {
const val = verificationCodeInput.join('');
if (val.length === 6) {
if (val === code.value) {
ElNotification({
title: '验证成功',
message: '欢迎回来',
type: 'success',
});
data.showName = 'success';
} else {
ElNotification({
title: '验证失败',
message: '您输入的验证码有误',
type: 'error',
});
verificationCodeInput.fill('');
this.$refs['codeInput0']?.focus();
}
}
},
resendCode() {
code.value = createCode();
ElNotification({
title: '发送成功',
message: '您的验证码为' + code.value,
type: 'success',
});
},
};
},
});
app.component('success', {
template: '#success',
});
app.mount('#app');
</script>
</html>