效果如上~
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>滑块拼图验证码 - 拖动条 Loading 效果和验证后禁用</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.slider-container {
width: 300px;
height: 40px;
margin: 0 auto;
background-color: #f0f0f0;
position: relative;
user-select: none;
}
.slider-mask {
position: absolute;
left: 0;
top: 0;
height: 40px;
border: 0;
background: #D1E9FE;
}
.slider-button {
position: absolute;
left: 0;
top: 0;
width: 40px;
height: 38px;
background: #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: background .2s linear;
}
.slider-text {
position: absolute;
text-align: center;
width: 100%;
height: 40px;
line-height: 40px;
font-size: 14px;
color: #45494c;
z-index: 2;
}
.puzzle-container {
position: relative;
width: 300px;
height: 150px;
margin: 0 auto 20px;
overflow: hidden;
}
.puzzle-image, .puzzle-piece {
position: absolute;
top: 0;
left: 0;
}
.refresh-button {
margin-top: 10px;
padding: 5px 10px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
.debug-info {
margin-top: 20px;
font-size: 14px;
color: #666;
}
.loading-slider {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.verified .slider-button {
background-color: #4CAF50;
}
.verified .slider-mask {
background-color: #a5d6a7;
}
</style>
</head>
<body>
<div id="app">
<h1>滑块拼图验证码 - 拖动条 Loading 效果和验证后禁用</h1>
<div class="puzzle-container">
<canvas ref="bgCanvas" width="300" height="150"></canvas>
<canvas ref="pieceCanvas" width="300" height="150" class="puzzle-piece" :style="puzzlePieceStyle"></canvas>
</div>
<div class="slider-container"
:class="{ 'verified': isVerified }"
@mousedown="startSlide"
@mousemove="moveSlide"
@mouseup="endSlide"
@mouseleave="endSlide">
<div class="slider-mask" :style="{ width: sliderLeft + 'px' }"></div>
<div class="slider-button" :style="{ left: sliderLeft + 'px' }"></div>
<div class="slider-text">{{ sliderText }}</div>
<div v-if="isLoading" class="loading-slider">
<div class="loading-spinner"></div>
</div>
</div>
<button @click="refreshPuzzle" class="refresh-button">刷新验证码</button>
<div class="debug-info">
<p>目标位置: {{ targetLeft }}</p>
<p>当前位置: {{ puzzlePieceLeft }}</p>
<p>误差: {{ Math.abs(targetLeft - puzzlePieceLeft) }}</p>
<p>容差: {{ tolerance }}</p>
</div>
</div>
<script>
// 模拟后端API
const mockBackendAPI = {
getPuzzleData() {
const images = [
'1 (1).jpg',
'1 (2).png',
'1 (3).png'
];
const randomImage = images[Math.floor(Math.random() * images.length)];
return {
image: randomImage,
pieceX: Math.floor(Math.random() * 150) + 50, // 50 to 200
pieceY: Math.floor(Math.random() * 50) + 20, // 20 to 70
};
},
verifyPuzzle(submittedX, actualX, tolerance) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(Math.abs(submittedX - actualX) <= tolerance);
}, 1000); // 模拟网络延迟
});
}
};
new Vue({
el: '#app',
data: {
puzzleImage: null,
sliderLeft: 0,
isSliding: false,
startX: 0,
puzzlePieceLeft: 0,
targetLeft: 0,
targetTop: 0,
sliderText: '向右滑动完成拼图',
pieceWidth: 50,
pieceHeight: 50,
pieceShape: null,
tolerance: 10,
isLoading: false,
isVerified: false
},
computed: {
puzzlePieceStyle() {
return {
transform: `translateX(${this.puzzlePieceLeft}px)`,
}
}
},
mounted() {
this.refreshPuzzle();
},
methods: {
refreshPuzzle() {
this.isVerified = false;
const puzzleData = mockBackendAPI.getPuzzleData();
this.loadImage(puzzleData.image).then(img => {
this.puzzleImage = img;
this.targetLeft = puzzleData.pieceX;
this.targetTop = puzzleData.pieceY;
this.sliderLeft = 0;
this.puzzlePieceLeft = 0;
this.sliderText = '向右滑动完成拼图';
this.generatePieceShape();
this.drawPuzzle();
});
},
loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
},
generatePieceShape() {
const canvas = document.createElement('canvas');
canvas.width = this.pieceWidth;
canvas.height = this.pieceHeight;
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(this.pieceWidth, 0);
ctx.lineTo(this.pieceWidth, this.pieceHeight);
ctx.lineTo(0, this.pieceHeight);
ctx.closePath();
ctx.fill();
this.pieceShape = canvas;
},
drawPuzzle() {
const bgCtx = this.$refs.bgCanvas.getContext('2d');
const pieceCtx = this.$refs.pieceCanvas.getContext('2d');
// 清空画布
bgCtx.clearRect(0, 0, 300, 150);
pieceCtx.clearRect(0, 0, 300, 150);
// 绘制背景图
bgCtx.drawImage(this.puzzleImage, 0, 0, 300, 150);
// 在背景图上绘制黑色缺口
bgCtx.fillStyle = 'rgba(0, 0, 0, 0.7)';
bgCtx.fillRect(this.targetLeft, this.targetTop, this.pieceWidth, this.pieceHeight);
// 绘制拼图块
pieceCtx.drawImage(
this.puzzleImage,
this.targetLeft, this.targetTop, this.pieceWidth, this.pieceHeight,
0, this.targetTop, this.pieceWidth, this.pieceHeight
);
// 绘制拼图块边框
pieceCtx.strokeStyle = 'white';
pieceCtx.lineWidth = 2;
pieceCtx.strokeRect(0, this.targetTop, this.pieceWidth, this.pieceHeight);
},
startSlide(e) {
if (this.isVerified) return;
this.isSliding = true;
this.startX = e.clientX - this.sliderLeft;
},
moveSlide(e) {
if (!this.isSliding || this.isVerified) return;
let moveX = e.clientX - this.startX;
if (moveX < 0) moveX = 0;
if (moveX > 250) moveX = 250; // 限制最大滑动距离
this.sliderLeft = moveX;
this.puzzlePieceLeft = moveX;
},
async endSlide() {
if (!this.isSliding || this.isVerified) return;
this.isSliding = false;
this.isLoading = true; // 显示 loading 效果
const isValid = await mockBackendAPI.verifyPuzzle(this.puzzlePieceLeft, this.targetLeft, this.tolerance);
this.isLoading = false; // 隐藏 loading 效果
if (isValid) {
this.sliderText = '验证成功!';
this.sliderLeft = 250; // 滑块移动到最右侧
this.isVerified = true;
} else {
this.sliderLeft = 0;
this.puzzlePieceLeft = 0;
this.sliderText = '验证失败,请重试';
}
this.drawPuzzle();
}
}
});
</script>
</body>
</html>