|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,20 @@ |
|||||||
|
import Vue from 'vue' |
||||||
|
import Main from './main.vue' |
||||||
|
import {PopupManager} from 'element-ui/src/utils/popup' |
||||||
|
|
||||||
|
let Constructor = Vue.extend(Main) |
||||||
|
|
||||||
|
const PuzzleVerify = function ({left = 0, top = 0} = {}) { |
||||||
|
const instance = new Constructor({data: {positionLeft: left}}) |
||||||
|
instance.positionTop = top - Math.round((instance.height + 130) / 2) |
||||||
|
instance.$mount() |
||||||
|
document.body.appendChild(instance.$el) |
||||||
|
instance.$el.style.zIndex = PopupManager.nextZIndex() |
||||||
|
instance.visible = true |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
instance.resolve = resolve |
||||||
|
instance.reject = reject |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export default PuzzleVerify |
||||||
@ -1,370 +0,0 @@ |
|||||||
<template> |
|
||||||
<div class="puzzle-container"> |
|
||||||
<div class="puzzle-header"> |
|
||||||
<span class="puzzle-header-title">拖动下方滑块完成拼图</span> |
|
||||||
<div class="puzzle-header-tool"> |
|
||||||
<el-button @click="refreshImg" icon="el-icon-refresh" size="small" type="text"/> |
|
||||||
<el-button @click="$emit('close')" icon="el-icon-close" size="small" type="text"/> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div :style="'width:' + width + 'px'" class="puzzle-view"> |
|
||||||
<img :src="imgRandom" |
|
||||||
:style="'width:' + width + 'px;height:' + height + 'px;'" |
|
||||||
id="scream" |
|
||||||
ref="scream" |
|
||||||
/> |
|
||||||
<canvas :height="height" :width="width" id="puzzle-box" ref="puzzleBox"/> |
|
||||||
<div :style="'left:' + left_Num+ 'px'" class="puzzle-lost-box"> |
|
||||||
<canvas :height="height" :width="width" id="puzzle-shadow" ref="puzzleShadow"/> |
|
||||||
<canvas :height="height" :width="width" id="puzzle-lost" ref="puzzleLost"/> |
|
||||||
</div> |
|
||||||
<span :class="{'hide':displayTips}" class="verify-result" ref="verTips"> |
|
||||||
<span class="success" v-show="verification"> |
|
||||||
<i class="el-icon-circle-check"/>验证通过 |
|
||||||
</span> |
|
||||||
<span class="error" v-show="!verification"> |
|
||||||
<i class="el-icon-circle-close"/>验证失败 |
|
||||||
</span> |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="slider-container"> |
|
||||||
<div class="slider-bar"/> |
|
||||||
<el-button @mousedown.native.prevent.stop="startMove" |
|
||||||
circle |
|
||||||
class="slider-btn" |
|
||||||
icon="el-icon-rank" |
|
||||||
ref="sliderBtn" |
|
||||||
type="success"/> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script> |
|
||||||
/* |
|
||||||
* https://github.com/Kevin-269581661/vue-puzzle-verification |
|
||||||
* 网上搬的例子,实际开发建议用极验 |
|
||||||
* */ |
|
||||||
export default { |
|
||||||
name: "PuzzleVerify", |
|
||||||
props: { |
|
||||||
// 画布图片的尺寸 |
|
||||||
width: { |
|
||||||
type: Number, |
|
||||||
default: 260 |
|
||||||
}, |
|
||||||
height: { |
|
||||||
type: Number, |
|
||||||
default: 120 |
|
||||||
}, |
|
||||||
// 图集 |
|
||||||
puzzleImgList: { |
|
||||||
type: Array, |
|
||||||
default: () => [] |
|
||||||
}, |
|
||||||
// 滑块的大小 |
|
||||||
blockSize: { |
|
||||||
type: Number, |
|
||||||
default: 40 |
|
||||||
}, |
|
||||||
// 误差 |
|
||||||
deviation: { |
|
||||||
type: Number, |
|
||||||
default: 4 |
|
||||||
}, |
|
||||||
// 滑块随机出现的范围 |
|
||||||
padding: { |
|
||||||
type: Number, |
|
||||||
default: 20 |
|
||||||
} |
|
||||||
}, |
|
||||||
data() { |
|
||||||
return { |
|
||||||
moveStart: null, |
|
||||||
displayTips: false, |
|
||||||
verification: false, |
|
||||||
randomX: null, |
|
||||||
randomY: null, |
|
||||||
imgRandom: "", |
|
||||||
left_Num: 0 |
|
||||||
} |
|
||||||
}, |
|
||||||
methods: { |
|
||||||
//刷新 |
|
||||||
refreshImg() { |
|
||||||
let imgRandomIndex = Math.round(Math.random() * (this.puzzleImgList.length - 1)) |
|
||||||
this.imgRandom = this.puzzleImgList[imgRandomIndex] |
|
||||||
this.initCanvas() |
|
||||||
}, |
|
||||||
//画布初始化 |
|
||||||
initCanvas() { |
|
||||||
this.clearCanvas() |
|
||||||
let w = this.width |
|
||||||
let h = this.height |
|
||||||
let PL_Size = this.blockSize |
|
||||||
let padding = this.padding |
|
||||||
let MinN_X = padding + PL_Size |
|
||||||
let MaxN_X = w - padding - PL_Size - PL_Size / 6 |
|
||||||
let MaxN_Y = padding |
|
||||||
let MinN_Y = h - padding - PL_Size - PL_Size / 6 |
|
||||||
this.randomX = Math.round(Math.random() * (MaxN_X - PL_Size) + MinN_X) |
|
||||||
this.randomY = Math.round(Math.random() * MaxN_Y + MinN_Y) |
|
||||||
let X = this.randomX |
|
||||||
let Y = this.randomY |
|
||||||
this.left_Num = -X + 10 |
|
||||||
let d = PL_Size / 3 |
|
||||||
|
|
||||||
let c = this.$refs.puzzleBox |
|
||||||
let c_l = this.$refs.puzzleLost |
|
||||||
let c_s = this.$refs.puzzleShadow |
|
||||||
let ctx = c.getContext("2d") |
|
||||||
let ctx_l = c_l.getContext("2d") |
|
||||||
let ctx_s = c_s.getContext("2d") |
|
||||||
ctx.globalCompositeOperation = "xor" |
|
||||||
ctx.shadowBlur = 10 |
|
||||||
ctx.shadowColor = "#fff" |
|
||||||
ctx.shadowOffsetX = 3 |
|
||||||
ctx.shadowOffsetY = 3 |
|
||||||
ctx.fillStyle = "rgba(0,0,0,0.7)" |
|
||||||
ctx.beginPath() |
|
||||||
ctx.lineWidth = 1 |
|
||||||
ctx.strokeStyle = "rgba(0,0,0,0)" |
|
||||||
|
|
||||||
ctx.moveTo(X, Y) |
|
||||||
ctx.lineTo(X + d, Y) |
|
||||||
ctx.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y) |
|
||||||
ctx.lineTo(X + 3 * d, Y) |
|
||||||
ctx.lineTo(X + 3 * d, Y + d) |
|
||||||
ctx.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d) |
|
||||||
ctx.lineTo(X + 3 * d, Y + 3 * d) |
|
||||||
ctx.lineTo(X, Y + 3 * d) |
|
||||||
|
|
||||||
ctx.closePath() |
|
||||||
ctx.stroke() |
|
||||||
ctx.fill() |
|
||||||
|
|
||||||
let img = new Image() |
|
||||||
img.src = this.imgRandom |
|
||||||
|
|
||||||
img.onload = function () { |
|
||||||
ctx_l.drawImage(img, 0, 0, w, h) |
|
||||||
} |
|
||||||
ctx_l.beginPath() |
|
||||||
ctx_l.strokeStyle = "rgba(0,0,0,0)" |
|
||||||
|
|
||||||
ctx_l.moveTo(X, Y) |
|
||||||
ctx_l.lineTo(X + d, Y) |
|
||||||
ctx_l.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y) |
|
||||||
ctx_l.lineTo(X + 3 * d, Y) |
|
||||||
ctx_l.lineTo(X + 3 * d, Y + d) |
|
||||||
ctx_l.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d) |
|
||||||
ctx_l.lineTo(X + 3 * d, Y + 3 * d) |
|
||||||
ctx_l.lineTo(X, Y + 3 * d) |
|
||||||
|
|
||||||
ctx_l.closePath() |
|
||||||
ctx_l.stroke() |
|
||||||
ctx_l.shadowBlur = 10 |
|
||||||
ctx_l.shadowColor = "black" |
|
||||||
ctx_l.clip() |
|
||||||
ctx_s.beginPath() |
|
||||||
ctx_s.lineWidth = 1 |
|
||||||
ctx_s.strokeStyle = "rgba(0,0,0,0)" |
|
||||||
|
|
||||||
ctx_s.moveTo(X, Y) |
|
||||||
ctx_s.lineTo(X + d, Y) |
|
||||||
ctx_s.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y) |
|
||||||
ctx_s.lineTo(X + 3 * d, Y) |
|
||||||
ctx_s.lineTo(X + 3 * d, Y + d) |
|
||||||
ctx_s.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d) |
|
||||||
ctx_s.lineTo(X + 3 * d, Y + 3 * d) |
|
||||||
ctx_s.lineTo(X, Y + 3 * d) |
|
||||||
|
|
||||||
ctx_s.closePath() |
|
||||||
ctx_s.stroke() |
|
||||||
ctx_s.shadowBlur = 20 |
|
||||||
ctx_s.shadowColor = "black" |
|
||||||
ctx_s.fill() |
|
||||||
}, |
|
||||||
//通过重置画布尺寸清空画布,这种方式更彻底 |
|
||||||
clearCanvas() { |
|
||||||
let c = this.$refs.puzzleBox |
|
||||||
let c_l = this.$refs.puzzleLost |
|
||||||
let c_s = this.$refs.puzzleShadow |
|
||||||
c.setAttribute("height", c.getAttribute("height")) |
|
||||||
c_l.setAttribute("height", c.getAttribute("height")) |
|
||||||
c_s.setAttribute("height", c.getAttribute("height")) |
|
||||||
}, |
|
||||||
//按住滑块后初始化移动监听,记录初始位置 |
|
||||||
startMove(e) { |
|
||||||
this.moveStart = e.pageX |
|
||||||
this.addMouseMoveListener() |
|
||||||
}, |
|
||||||
//滑块移动 |
|
||||||
moving(e) { |
|
||||||
let moveX = e.pageX |
|
||||||
let d = moveX - this.moveStart |
|
||||||
if (this.moveStart === null |
|
||||||
|| d < 0 |
|
||||||
|| d > this.width - this.padding - this.blockSize) return |
|
||||||
this.$refs.sliderBtn.$el.style.left = d + "px" |
|
||||||
this.$refs.sliderBtn.$el.style.transition = "inherit" |
|
||||||
this.$refs.puzzleLost.style.left = d + "px" |
|
||||||
this.$refs.puzzleLost.style.transition = "inherit" |
|
||||||
this.$refs.puzzleShadow.style.left = d + "px" |
|
||||||
this.$refs.puzzleShadow.style.transition = "inherit" |
|
||||||
}, |
|
||||||
//移动结束,验证并回调 |
|
||||||
moveEnd(e) { |
|
||||||
let moveEnd_X = e.pageX - this.moveStart |
|
||||||
let ver_Num = this.randomX - 10 |
|
||||||
let Min_left = ver_Num - this.deviation |
|
||||||
let Max_left = ver_Num + this.deviation |
|
||||||
if (this.moveStart !== null) { |
|
||||||
if (Max_left > moveEnd_X && moveEnd_X > Min_left) { |
|
||||||
this.displayTips = true |
|
||||||
this.verification = true |
|
||||||
setTimeout(() => { |
|
||||||
this.displayTips = false |
|
||||||
this.initCanvas() |
|
||||||
this.$emit('success') |
|
||||||
}, 500) |
|
||||||
} |
|
||||||
else { |
|
||||||
this.displayTips = true |
|
||||||
this.verification = false |
|
||||||
setTimeout(() => { |
|
||||||
this.displayTips = false |
|
||||||
this.initCanvas() |
|
||||||
this.$emit('fail') |
|
||||||
}, 800) |
|
||||||
} |
|
||||||
} |
|
||||||
setTimeout(() => { |
|
||||||
this.$refs.sliderBtn.$el.style.left = '0' |
|
||||||
this.$refs.sliderBtn.$el.style.transition = "left 0.5s" |
|
||||||
this.$refs.puzzleLost.style.left = '0' |
|
||||||
this.$refs.puzzleLost.style.transition = "left 0.5s" |
|
||||||
this.$refs.puzzleShadow.style.left = '0' |
|
||||||
this.$refs.puzzleShadow.style.transition = "left 0.5s" |
|
||||||
}, 400) |
|
||||||
this.moveStart = null |
|
||||||
this.removeMouseMoveListener() |
|
||||||
}, |
|
||||||
//全局绑定滑块移动与滑动结束,移动过程中鼠标可在页面任何位置 |
|
||||||
addMouseMoveListener() { |
|
||||||
document.addEventListener("mousemove", this.moving) |
|
||||||
document.addEventListener("mouseup", this.moveEnd) |
|
||||||
}, |
|
||||||
removeMouseMoveListener() { |
|
||||||
document.removeEventListener("mousemove", this.moving) |
|
||||||
document.removeEventListener("mouseup", this.moveEnd) |
|
||||||
} |
|
||||||
}, |
|
||||||
created() { |
|
||||||
// 随机显示一张图片 |
|
||||||
let imgRandomIndex = Math.round(Math.random() * (this.puzzleImgList.length - 1)) |
|
||||||
this.imgRandom = this.puzzleImgList[imgRandomIndex] |
|
||||||
}, |
|
||||||
mounted() { |
|
||||||
this.$nextTick(() => this.initCanvas()) |
|
||||||
}, |
|
||||||
|
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss"> |
|
||||||
.puzzle-container { |
|
||||||
position: relative; |
|
||||||
display: inline-block; |
|
||||||
padding: 0 15px 28px; |
|
||||||
border: 1px solid #ddd; |
|
||||||
background: #ffffff; |
|
||||||
border-radius: 10px; |
|
||||||
|
|
||||||
.puzzle-header { |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: space-between; |
|
||||||
margin: 5px 0; |
|
||||||
|
|
||||||
.puzzle-header-title { |
|
||||||
font-size: 13px; |
|
||||||
color: #909399 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
.puzzle-view { |
|
||||||
position: relative; |
|
||||||
overflow: hidden; |
|
||||||
|
|
||||||
.puzzle-lost-box { |
|
||||||
position: absolute; |
|
||||||
left: 0; |
|
||||||
top: 0; |
|
||||||
} |
|
||||||
|
|
||||||
canvas { |
|
||||||
position: absolute; |
|
||||||
left: 0; |
|
||||||
top: 0; |
|
||||||
z-index: 22; |
|
||||||
} |
|
||||||
|
|
||||||
.verify-result { |
|
||||||
position: absolute; |
|
||||||
left: 0; |
|
||||||
bottom: -22px; |
|
||||||
background: rgba(255, 255, 255, 1); |
|
||||||
height: 22px; |
|
||||||
line-height: 22px; |
|
||||||
font-size: 12px; |
|
||||||
width: 100%; |
|
||||||
transition: bottom 0.4s; |
|
||||||
|
|
||||||
.success { |
|
||||||
color: #67C23A |
|
||||||
} |
|
||||||
|
|
||||||
.error { |
|
||||||
color: #F56C6C |
|
||||||
} |
|
||||||
|
|
||||||
i { |
|
||||||
margin: 0 10px; |
|
||||||
font-weight: bold; |
|
||||||
} |
|
||||||
|
|
||||||
&.hide { |
|
||||||
bottom: 0; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
.slider-container { |
|
||||||
position: relative; |
|
||||||
margin: 10px auto 0; |
|
||||||
min-height: 15px; |
|
||||||
|
|
||||||
.slider-bar { |
|
||||||
height: 10px; |
|
||||||
border: 1px solid #c3c3c3; |
|
||||||
border-radius: 5px; |
|
||||||
background: #e4e4e4; |
|
||||||
box-shadow: 0 1px 1px rgba(12, 10, 10, 0.2) inset; |
|
||||||
position: relative; |
|
||||||
top: 7px; |
|
||||||
} |
|
||||||
|
|
||||||
.slider-btn { |
|
||||||
position: absolute; |
|
||||||
width: 44px; |
|
||||||
height: 44px; |
|
||||||
left: 0; |
|
||||||
top: -10px; |
|
||||||
z-index: 12; |
|
||||||
transition: inherit; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -0,0 +1,519 @@ |
|||||||
|
<template> |
||||||
|
<transition :name="transition" @after-leave="handleAfterLeave"> |
||||||
|
<div |
||||||
|
v-show="visible" |
||||||
|
class="puzzle-container" |
||||||
|
:class="{'puzzle-shake':stat==='fail'}" |
||||||
|
:style="`left: ${positionLeft}px;top: ${positionTop}px;`" |
||||||
|
> |
||||||
|
<div class="puzzle-view" :style="`width:${width}px;height:${height}px;`"> |
||||||
|
<div v-show="!image.loading" style="width: 100%;height: 100%"> |
||||||
|
<img :src="image.src"/> |
||||||
|
<canvas ref="puzzleBox" :height="height" :width="width"/> |
||||||
|
<div class="puzzle-lost-box" :style="`left:${leftNum}px`"> |
||||||
|
<canvas ref="puzzleShadow" :style="sliderStyle" :height="height" :width="width"/> |
||||||
|
<canvas ref="puzzleLost" :style="sliderStyle" :height="height" :width="width"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<transition name="fade"> |
||||||
|
<div v-show="image.loading" class="puzzle-image-loading"> |
||||||
|
<div class="puzzle-image-loading__icon"/> |
||||||
|
<div class="puzzle-image-loading__text">加载中...</div> |
||||||
|
</div> |
||||||
|
</transition> |
||||||
|
<div :class="resultTipClass"> |
||||||
|
{{stat==='success'?'验证通过':'请正确拼合图像'}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="slider-container"> |
||||||
|
<div class="slider-track"> |
||||||
|
<div class="slider-tip" :style="stat==='ready'?'opacity: 1':''">拖动滑块完成拼图</div> |
||||||
|
</div> |
||||||
|
<div class="slider-btn" :style="sliderBtnStyle" @mousedown.prevent.stop="moveStart"/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="puzzle-footer"> |
||||||
|
<div class="puzzle-small"> |
||||||
|
<a class="puzzle-text-container puzzle-close" @click="close"> |
||||||
|
<div class="puzzle-text-tip">关闭验证</div> |
||||||
|
</a> |
||||||
|
<a class="puzzle-text-container puzzle-refresh" @click="refresh"> |
||||||
|
<div class="puzzle-text-tip">刷新验证</div> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</transition> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/* |
||||||
|
* https://github.com/Kevin-269581661/vue-puzzle-verification |
||||||
|
* 网上搬的例子,实际开发建议用极验 |
||||||
|
* */ |
||||||
|
export default { |
||||||
|
name: "PuzzleVerify", |
||||||
|
props: { |
||||||
|
//动画名称,具体请查看transition.scss |
||||||
|
transition: { |
||||||
|
type: String, |
||||||
|
default: 'scale' |
||||||
|
}, |
||||||
|
// 画布图片的尺寸 |
||||||
|
width: { |
||||||
|
type: Number, |
||||||
|
default: 260 |
||||||
|
}, |
||||||
|
height: { |
||||||
|
type: Number, |
||||||
|
default: 160 |
||||||
|
}, |
||||||
|
// 滑块的大小 |
||||||
|
blockSize: { |
||||||
|
type: Number, |
||||||
|
default: 40 |
||||||
|
}, |
||||||
|
// 误差 |
||||||
|
deviation: { |
||||||
|
type: Number, |
||||||
|
default: 4 |
||||||
|
}, |
||||||
|
// 滑块随机出现的范围 |
||||||
|
padding: { |
||||||
|
type: Number, |
||||||
|
default: 80 |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
visible: false, |
||||||
|
stat: 'ready',//ready,move,success,fail,error |
||||||
|
positionLeft: 0, |
||||||
|
positionTop: 0, |
||||||
|
resolve: () => ({}), |
||||||
|
reject: () => ({}), |
||||||
|
moveStartAtX: null, |
||||||
|
randomX: null, |
||||||
|
randomY: null, |
||||||
|
leftNum: 0, |
||||||
|
image: { |
||||||
|
src: '', |
||||||
|
loading: true |
||||||
|
}, |
||||||
|
sliderStyle: { |
||||||
|
left: '0', |
||||||
|
transition: 'inherit' |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
resultTipClass() { |
||||||
|
const showResultTip = ['success', 'fail'].includes(this.stat) |
||||||
|
return ['verify-result', `verify-${this.stat}`, showResultTip ? 'show-result' : ''] |
||||||
|
}, |
||||||
|
sliderBtnStyle() { |
||||||
|
return { |
||||||
|
...this.sliderStyle, |
||||||
|
'background-position': this.stat === 'move' ? '0 31.0992%' : '0 11.79625%' |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
close() { |
||||||
|
this.visible = false |
||||||
|
this.clear() |
||||||
|
this.reject() |
||||||
|
}, |
||||||
|
refresh() { |
||||||
|
this.initImage().then(() => this.initCanvas()) |
||||||
|
}, |
||||||
|
initCanvas() { |
||||||
|
this.clear() |
||||||
|
let MinX = this.padding + this.blockSize |
||||||
|
let MaxX = this.width - this.padding - 2 * this.blockSize - this.blockSize / 6 |
||||||
|
let MaxY = this.padding |
||||||
|
let MinY = this.height - this.padding - this.blockSize - this.blockSize / 6 |
||||||
|
this.randomX = Math.round(Math.random() * MaxX + MinX) |
||||||
|
this.randomY = Math.round(Math.random() * MaxY + MinY) |
||||||
|
let X = this.randomX |
||||||
|
let Y = this.randomY |
||||||
|
this.leftNum = -X + 10 |
||||||
|
let d = this.blockSize / 3 |
||||||
|
|
||||||
|
let c = this.$refs.puzzleBox |
||||||
|
let c_l = this.$refs.puzzleLost |
||||||
|
let c_s = this.$refs.puzzleShadow |
||||||
|
let ctx = c.getContext("2d") |
||||||
|
let ctx_l = c_l.getContext("2d") |
||||||
|
let ctx_s = c_s.getContext("2d") |
||||||
|
ctx.globalCompositeOperation = "xor" |
||||||
|
ctx.shadowBlur = 10 |
||||||
|
ctx.shadowColor = "#fff" |
||||||
|
ctx.shadowOffsetX = 3 |
||||||
|
ctx.shadowOffsetY = 3 |
||||||
|
ctx.fillStyle = "rgba(0,0,0,0.7)" |
||||||
|
ctx.beginPath() |
||||||
|
ctx.lineWidth = 1 |
||||||
|
ctx.strokeStyle = "rgba(0,0,0,0)" |
||||||
|
|
||||||
|
ctx.moveTo(X, Y) |
||||||
|
ctx.lineTo(X + d, Y) |
||||||
|
ctx.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y) |
||||||
|
ctx.lineTo(X + 3 * d, Y) |
||||||
|
ctx.lineTo(X + 3 * d, Y + d) |
||||||
|
ctx.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d) |
||||||
|
ctx.lineTo(X + 3 * d, Y + 3 * d) |
||||||
|
ctx.lineTo(X, Y + 3 * d) |
||||||
|
|
||||||
|
ctx.closePath() |
||||||
|
ctx.stroke() |
||||||
|
ctx.fill() |
||||||
|
|
||||||
|
this.drawImage(img => ctx_l.drawImage(img, 0, 0, this.width, this.height)) |
||||||
|
|
||||||
|
ctx_l.beginPath() |
||||||
|
ctx_l.strokeStyle = "rgba(0,0,0,0)" |
||||||
|
|
||||||
|
ctx_l.moveTo(X, Y) |
||||||
|
ctx_l.lineTo(X + d, Y) |
||||||
|
ctx_l.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y) |
||||||
|
ctx_l.lineTo(X + 3 * d, Y) |
||||||
|
ctx_l.lineTo(X + 3 * d, Y + d) |
||||||
|
ctx_l.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d) |
||||||
|
ctx_l.lineTo(X + 3 * d, Y + 3 * d) |
||||||
|
ctx_l.lineTo(X, Y + 3 * d) |
||||||
|
|
||||||
|
ctx_l.closePath() |
||||||
|
ctx_l.stroke() |
||||||
|
ctx_l.shadowBlur = 10 |
||||||
|
ctx_l.shadowColor = "black" |
||||||
|
ctx_l.clip() |
||||||
|
ctx_s.beginPath() |
||||||
|
ctx_s.lineWidth = 1 |
||||||
|
ctx_s.strokeStyle = "rgba(0,0,0,0)" |
||||||
|
|
||||||
|
ctx_s.moveTo(X, Y) |
||||||
|
ctx_s.lineTo(X + d, Y) |
||||||
|
ctx_s.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y) |
||||||
|
ctx_s.lineTo(X + 3 * d, Y) |
||||||
|
ctx_s.lineTo(X + 3 * d, Y + d) |
||||||
|
ctx_s.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d) |
||||||
|
ctx_s.lineTo(X + 3 * d, Y + 3 * d) |
||||||
|
ctx_s.lineTo(X, Y + 3 * d) |
||||||
|
|
||||||
|
ctx_s.closePath() |
||||||
|
ctx_s.stroke() |
||||||
|
ctx_s.shadowBlur = 20 |
||||||
|
ctx_s.shadowColor = "black" |
||||||
|
ctx_s.fill() |
||||||
|
}, |
||||||
|
initImage() { |
||||||
|
const imageListTemp = [ |
||||||
|
"https://dss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=4018557288,1217151095&fm=26&gp=0.jpg", |
||||||
|
"https://dss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3252521864,872614242&fm=26&gp=0.jpg", |
||||||
|
"https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2534506313,1688529724&fm=26&gp=0.jpg" |
||||||
|
] |
||||||
|
this.image.loading = true |
||||||
|
let imgRandomIndex = Math.round(Math.random() * (imageListTemp.length - 1)) |
||||||
|
this.image.src = imageListTemp[imgRandomIndex] |
||||||
|
return new Promise(resolve => resolve()) |
||||||
|
}, |
||||||
|
drawImage(fun) { |
||||||
|
let img = new Image() |
||||||
|
img.src = this.image.src |
||||||
|
img.onload = () => { |
||||||
|
fun(img) |
||||||
|
this.image.loading = false |
||||||
|
} |
||||||
|
}, |
||||||
|
clear() { |
||||||
|
this.stat = 'ready' |
||||||
|
let c = this.$refs.puzzleBox |
||||||
|
let c_l = this.$refs.puzzleLost |
||||||
|
let c_s = this.$refs.puzzleShadow |
||||||
|
c.setAttribute("height", c.getAttribute("height")) |
||||||
|
c_l.setAttribute("height", c.getAttribute("height")) |
||||||
|
c_s.setAttribute("height", c.getAttribute("height")) |
||||||
|
}, |
||||||
|
moveStart(e) { |
||||||
|
this.moveStartAtX = e.pageX |
||||||
|
this.addListener() |
||||||
|
}, |
||||||
|
moving(e) { |
||||||
|
this.stat = 'move' |
||||||
|
const distance = e.pageX - this.moveStartAtX |
||||||
|
if (this.moveStartAtX === null |
||||||
|
|| distance < 0 |
||||||
|
|| distance > this.width - this.blockSize) { |
||||||
|
return |
||||||
|
} |
||||||
|
this.sliderStyle.left = distance + "px" |
||||||
|
this.sliderStyle.transition = "inherit" |
||||||
|
}, |
||||||
|
moveEnd(e) { |
||||||
|
const distance = e.pageX - this.moveStartAtX |
||||||
|
const ver_Num = this.randomX - 10 |
||||||
|
const minLeft = ver_Num - this.deviation |
||||||
|
const maxLeft = ver_Num + this.deviation |
||||||
|
if (this.moveStartAtX !== null) { |
||||||
|
const success = maxLeft > distance && distance > minLeft |
||||||
|
this.stat = success ? 'success' : 'fail' |
||||||
|
setTimeout(() => { |
||||||
|
this.stat = 'ready' |
||||||
|
if (!success) return this.initCanvas() |
||||||
|
this.resolve() |
||||||
|
this.close() |
||||||
|
}, success ? 500 : 1000) |
||||||
|
} |
||||||
|
setTimeout(() => { |
||||||
|
this.sliderStyle.left = '0' |
||||||
|
this.sliderStyle.transition = "left 0.5s" |
||||||
|
}, 1000) |
||||||
|
this.moveStartAtX = null |
||||||
|
this.removeListener() |
||||||
|
}, |
||||||
|
addListener() { |
||||||
|
if (this.image.loading) return |
||||||
|
document.addEventListener("mousemove", this.moving) |
||||||
|
document.addEventListener("mouseup", this.moveEnd) |
||||||
|
}, |
||||||
|
removeListener() { |
||||||
|
document.removeEventListener("mousemove", this.moving) |
||||||
|
document.removeEventListener("mouseup", this.moveEnd) |
||||||
|
}, |
||||||
|
handleAfterLeave() { |
||||||
|
this.$destroy(true) |
||||||
|
this.$el.parentNode.removeChild(this.$el) |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.initImage().then(() => this.initCanvas()) |
||||||
|
}, |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.puzzle-container { |
||||||
|
position: absolute; |
||||||
|
box-shadow: 0 0 10px #cccccc; |
||||||
|
border: 1px solid #cccccc; |
||||||
|
background: #ffffff; |
||||||
|
|
||||||
|
.puzzle-text-container { |
||||||
|
&:hover { |
||||||
|
overflow: visible !important; |
||||||
|
} |
||||||
|
|
||||||
|
.puzzle-text-tip { |
||||||
|
position: absolute; |
||||||
|
top: -32px; |
||||||
|
left: 10px; |
||||||
|
border-radius: 2px; |
||||||
|
padding: 0 4px; |
||||||
|
height: 22px; |
||||||
|
min-width: 50px; |
||||||
|
line-height: 22px; |
||||||
|
background-color: #5F5F5F; |
||||||
|
white-space: nowrap; |
||||||
|
font-size: 12px; |
||||||
|
text-align: center; |
||||||
|
color: white; |
||||||
|
|
||||||
|
&::before { |
||||||
|
display: block; |
||||||
|
position: absolute; |
||||||
|
bottom: -6px; |
||||||
|
left: 0; |
||||||
|
content: ''; |
||||||
|
border-style: solid; |
||||||
|
border-width: 4px 6px; |
||||||
|
border-color: #5F5F5F transparent transparent #5F5F5F; |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.puzzle-view { |
||||||
|
position: relative; |
||||||
|
overflow: hidden; |
||||||
|
margin: 10px 10px 0; |
||||||
|
|
||||||
|
.puzzle-image-loading { |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
background-color: #e5e5e5; |
||||||
|
padding-top: 10%; |
||||||
|
|
||||||
|
&__icon { |
||||||
|
background-size: 764.70588%; |
||||||
|
background-position: 0 70.70218%; |
||||||
|
margin: 11% auto 10px; |
||||||
|
width: 34px; |
||||||
|
height: 26px; |
||||||
|
overflow: hidden; |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-image: url(~@/assets/images/sprite.1.2.4.png); |
||||||
|
} |
||||||
|
|
||||||
|
&__text { |
||||||
|
text-align: center; |
||||||
|
font-size: 14px; |
||||||
|
margin-bottom: 1%; |
||||||
|
color: #b2b2b2; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
img { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
object-fit: cover; |
||||||
|
} |
||||||
|
|
||||||
|
.puzzle-lost-box { |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
} |
||||||
|
|
||||||
|
canvas { |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
z-index: 22; |
||||||
|
} |
||||||
|
|
||||||
|
.verify-result { |
||||||
|
position: absolute; |
||||||
|
z-index: 999; |
||||||
|
color: white; |
||||||
|
height: 24px; |
||||||
|
line-height: 24px; |
||||||
|
left: 0; |
||||||
|
bottom: -25px; |
||||||
|
font-size: 14px; |
||||||
|
width: 100%; |
||||||
|
transition: bottom 0.3s; |
||||||
|
text-indent: 16px; |
||||||
|
|
||||||
|
&.verify-success { |
||||||
|
background-color: $--color-success |
||||||
|
} |
||||||
|
|
||||||
|
&.verify-fail { |
||||||
|
background-color: $--color-danger |
||||||
|
} |
||||||
|
|
||||||
|
&.show-result { |
||||||
|
bottom: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.slider-container { |
||||||
|
position: relative; |
||||||
|
margin: 5.39% 3.24%; |
||||||
|
width: 93.52%; |
||||||
|
padding: 0 0 13.67% 0; |
||||||
|
background-color: white; |
||||||
|
background-size: 100%; |
||||||
|
background-position: 0 0; |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-image: url(~@/assets/images/sprite.1.2.4.png); |
||||||
|
|
||||||
|
.slider-track { |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
margin: -19px 0 0 0; |
||||||
|
padding: 0 0 0 25%; |
||||||
|
|
||||||
|
.slider-tip { |
||||||
|
position: relative; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
opacity: 0; |
||||||
|
transition: opacity .5s; |
||||||
|
line-height: 38px; |
||||||
|
font-size: 14px; |
||||||
|
text-align: center; |
||||||
|
white-space: nowrap; |
||||||
|
color: #88949d; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.slider-btn { |
||||||
|
position: absolute; |
||||||
|
margin: -4.62% 0 0 -2.31%; |
||||||
|
width: 25.38%; |
||||||
|
padding: 0 0 25.38% 0; |
||||||
|
cursor: pointer; |
||||||
|
background-size: 393.93939%; |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-image: url(~@/assets/images/sprite.1.2.4.png); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.puzzle-footer { |
||||||
|
position: relative; |
||||||
|
border-top: 1px solid #EEEEEE; |
||||||
|
width: 100%; |
||||||
|
margin: 0; |
||||||
|
padding: 0 0 17.27% 0; |
||||||
|
|
||||||
|
.puzzle-small { |
||||||
|
position: absolute; |
||||||
|
left: 5.1%; |
||||||
|
top: 50%; |
||||||
|
margin-top: -4.13%; |
||||||
|
padding: 0 0 8.27% 0; |
||||||
|
width: 40.45%; |
||||||
|
height: 0; |
||||||
|
|
||||||
|
.puzzle-close, .puzzle-refresh { |
||||||
|
margin-left: 8.9%; |
||||||
|
width: 17.8%; |
||||||
|
cursor: pointer; |
||||||
|
text-decoration: none; |
||||||
|
vertical-align: top; |
||||||
|
position: relative; |
||||||
|
display: inline-block; |
||||||
|
height: 0; |
||||||
|
padding-bottom: 17.8%; |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-image: url(~@/assets/images/sprite.1.2.4.png); |
||||||
|
overflow: hidden; |
||||||
|
background-size: 1300%; |
||||||
|
} |
||||||
|
|
||||||
|
.puzzle-close { |
||||||
|
margin-left: 0; |
||||||
|
background-position: 0 44.86874%; |
||||||
|
} |
||||||
|
|
||||||
|
.puzzle-refresh { |
||||||
|
background-position: 0 81.38425%; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.puzzle-shake { |
||||||
|
animation: shake 0.2s linear 3 both; |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes shake { |
||||||
|
25% { |
||||||
|
transform: translate3d(-6px, 0, 0) |
||||||
|
} |
||||||
|
75% { |
||||||
|
transform: translate3d(6px, 0, 0) |
||||||
|
} |
||||||
|
100% { |
||||||
|
transform: translate3d(0, 0, 0) |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -1,20 +0,0 @@ |
|||||||
<template> |
|
||||||
<div> |
|
||||||
<el-row class="tip-row"> |
|
||||||
<a href="https://github.com/Kevin-269581661/vue-puzzle-verification" target="_blank">网上搬的例子</a> |
|
||||||
</el-row> |
|
||||||
<puzzle-verification :puzzleImgList="puzzleImgList"/> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
<script> |
|
||||||
import PuzzleVerification from '@/components/PuzzleVerify' |
|
||||||
|
|
||||||
export default { |
|
||||||
components: {PuzzleVerification}, |
|
||||||
data() { |
|
||||||
return { |
|
||||||
puzzleImgList: ['http://q4r31dqam.bkt.clouddn.com/image/56.jpg'] |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||