图片验证码

介绍

在我们访问一些网站的时候,常常会遇到让我们选择图片来验证是否是人类的验证码,利用这种方式网站可以过滤掉来自机器的恶意访问。

本题请实现一个带图片验证码功能的登录页面。

准备

开始答题前,需要先打开本题的项目代码文件夹,目录结构如下:

1
2
3
4
5
6
7
8
9
10
├── css
│ └── style.css
├── effect.gif
├── images
├── index.html
├── initial.gif
└── js
├── axios.js
├── data.json
└── vue.js

其中:

  • css/style.css 是样式文件。
  • index.html 是主页面。
  • images 文件夹内包含了页面使用的图片文件。
  • js/vue.jsvue 文件。
  • js/axios.jsaxios 文件。
  • js/data.json 是图片数据文件。
  • effect.gif 是页面最终的效果图。
  • initial.gif 是页面的初始效果图。

注意:打开环境后发现缺少项目代码,请复制下述命令至命令行进行下载:

1
2
3
cd /home/project
wget https://labfile.oss.aliyuncs.com/courses/18421/pic.zip
unzip pic.zip && rm pic.zip

在浏览器中预览 index.html 页面,初始交互效果可参考文件夹下面的 gif 图,图片名称为 initial.gif(提示:可以通过 VS Code 或者浏览器预览 gif 图片)。

初始交互效果

目标

请在 index.html 文件中补全代码,具体需求如下:

  1. 修改 HTML 模版和 script 脚本内容,实现当用户点击某张图片时给图片容器(即 .image-container 元素)添加 selected 类,如果再次点击则去掉该类。当用户没有选择任何图片时,右下方的按钮(即 #check-btn 元素)显示跳过,反之显示提交

  2. 当用户点击右下方按钮(即 #check-btn 元素)时,对用户选择的正确性进行判断。规则如下:

    • 用户必须选中所有正确的图片,并且不能有错误的图片被选(图片的正误可以根据 data.json 中图片的 type 属性判断)。

    data.json 参数说明

    | 参数 | 说明 | 类型 |
    | ————— | ————————— | ———- |
    | path | 图片路径 | string |
    | type | 图片是否是正确答案 | boolean |
    | category | 图片对应的问题类别 | number |

    • 如果用户选择错误,则展示验证失败,请重试的提示,并让用户继续选择;反之展示验证成功的提示,并将弹窗关闭(即 .modal 元素),同时隐藏验证码开启按钮(即 .box 元素),并显示登录(即 #login 元素)按钮。

最终效果可参考文件夹下面的 gif 图,图片名称为 effect.gif(提示:可以通过 VS Code 或者浏览器预览 gif 图片)。

最终效果

规定

  • 请严格按照考试步骤操作,切勿修改考试默认提供项目中的文件名称、文件夹路径、class 名、id 名、图片名等,以免造成无法判题通过。
  • 满足需求后,保持 Web 服务处于可以正常访问状态,点击「提交检测」系统会自动检测。

判分标准

  • 完成目标 1,得 5 分。
  • 完成目标 2,得 15 分。

总通过次数: 17 | 总提交次数: 13 | 通过率: 130.8%

难度: 中等 标签: 蓝桥杯, 2023, 国赛, Web 前端, Vue.js

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8" />
<title>图片验证码</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="js/vue.js" type="text/javascript" charset="utf-8"></script>
<script src="js/axios.js"></script>
</head>

<body>
<div id="app">
<header>
<h1>图片验证码</h1>
</header>
<div class="container">
<form action="" @submit.prevent="">
<label for="id">账号:</label>
<input type="text" name="id">
<label for="password">密码:</label>
<input type="password" name="password">
<div class="box" v-if="showBox">
<div class="checkbox" @click="startCheck"></div>
<span>我是人类</span>
</div>
<button id="login" type="submit" v-if="showLogin">登录</button>
</form>
</div>
<div class="modal" v-if="showModal">
<div class="head">
<h2>请点击所有包含{{ hint }}的图片</h2>
<p>如果没有,请点击"跳过"</p>
</div>
<div class="images">
<div class="image-container" v-for="(image, index) in images" @click="addSelected(index, $event)">
<img :src="image.path">
</div>
</div>
<div class="footer">
<img src="images/redo.svg" @click="startCheck">
<button id="check-btn" @click="check">{{ selectedIndexCounts ? '提交' : '跳过' }}</button>
</div>
</div>
<div class="toast" v-if="showToast">
{{ toastContent }}
</div>
</div>
</body>

<script>
var vm = new Vue({
el: "#app",
// TODO: 你可以根据需要添加新的data或computed属性
data: {
category: '',
toastContent: '',
allImages: [],
images: [],
showToast: false,
showModal: false,
showLogin: false,
showBox: true,
selectedIndexCounts: 0,
selectedIndexMap: {}
},
created() {
axios.get("./js/data.json").then(res => {
this.allImages = res.data;
})
},
computed: {
hint() {
return this.category === 1 ? '果汁' : '猫咪'
},
},
methods: {
// 展示验证图像模态框,随机选择一个问题类型和九张图片
startCheck() {
this.showModal = true;
this.category = Math.random() > 0.5 ? 1 : 2;
this.images = pick(this.allImages.filter(i => i.category === this.category), 9)
this.selectedIndexCounts = 0
this.selectedIndexMap = {}
document.querySelectorAll('.image-container').forEach(item => {
item.classList.contains('selected') ? item.classList.remove('selected') : ''
})
},
// TODO: 请修改以下代码实现用户选择正确与否的判断逻辑,可以添加新的方法
check() {
let result;
// 根据用户选择的正确与否展示相应的toast
result = this.images.every((item, index) => (item.type === false && !this.selectedIndexMap.hasOwnProperty(index) || (item.type === true) && this.selectedIndexMap.hasOwnProperty(index)));
if (result) {
this.toastContent = '验证成功'
this.showModal = false
this.showLogin = true
this.showBox = false
} else {
this.toastContent = '验证失败,请重试'
}
// 展示提示用户验证情况的Toast
this.showToast = true;
setTimeout(() => {
this.showToast = false;
}, 1500)
},
addSelected(index, event) {
if (event.currentTarget.classList.contains('selected')) {
event.currentTarget.classList.remove('selected')
this.selectedIndexCounts--
delete this.selectedIndexMap[index]
} else {
event.currentTarget.classList.add('selected')
this.selectedIndexCounts++
this.selectedIndexMap[index] = true
}

}
},

});

// 从数组中随机选择指定数量的元素
function pick(array, number) {
const s = new Set();
for (let i = 0; i < number; i++) {
const num = Math.floor(Math.random() * 20);
if (s.has(num)) {
i--;
} else {
s.add(num)
}
}
return Array.from(s).map(n => array[n]);
}
</script>

</html>