<template>
<view class="tf-Box-Bg">
<view class="tf-Box">
<view class="tf-Box-title">
<text class="text">请完成安全验证</text>
<u-icon name="close-circle" @click="close"></u-icon>
</view>
<view class="tf-content">
<canvas type="2d" :style="{ width:state.canvasW + 'px', height: state.canvasH + 'px' }"
class="tf-Box-center" canvas-id="tf-verify-canvas" id="tf-verify-canvas"></canvas>
<movable-area class="tf-Box-BtnBox" :style="{ width: state.canvasW + 'px' }">
<view class="tf-Box-BtnBox-text">滑动滑块完成拼图</view>
<movable-view class="tf-Box-BtnNei" :style="{ width: state.tfBoxBtnNeiElWidth + 'px' }"
direction="horizontal" :damping="50" :friction="50" :x="state.canvasX2" @change="changePath"
@touchend="endTouch" @mouseup="endTouch">
<view class="tf-Box-BtnNei-leftBox"
:style="{ backgroundColor: ImagesList[state.verifyIndex].color }">
</view>
</movable-view>
</movable-area>
</view>
</view>
</view>
</template>
<script setup>
// 定义emit向父组件传递的事件
const emit = defineEmits(['close', 'succeed']);
import {
ref,
reactive,
computed,
onMounted,
nextTick,
} from 'vue';
// mounted
// 接收props
const props = defineProps({
ImagesList: {
type: Array,
default: function() {
return [{
color: "#38a7b7",
src: "https://file.ggyx666.com/ggyx_uploads/20250728/7ade1d9decfa35643de864386c629a1c123458.jpg"
},
{
color: "#485967",
src: "https://file.ggyx666.com/ggyx_uploads/20250728/c590fd0a82429931b503fe418424b1dc123457.jpg"
}
]
}
}
})
const canvasX2 = ref(0) // 用于拼图块的初始X坐标
const state = reactive({
verifyIndex: 0, // 当前显示的验证图片的索引
canvasW: uni.upx2px(640), // canvas和滑块容器的宽度,单位转换为px
canvasH: uni.upx2px(640 / (16 / 9)),
canvasX2: 0, // 用于拼图块的初始X坐标
canvasX: 0, // 实时移动的X坐标,用于拼图块的位置
ctx: false, // canvas的绘图上下文
jgX: 0, // 拼图块的目标X位置(用于判断是否成功滑动到目标位置)
dqImgPath: '', //本地临时图片路径
RR: uni.upx2px(10), // 拼图块的半径,单位转换为px
YY: uni.upx2px(100), // 拼图块的Y轴坐标,单位转换为px
CS: uni.upx2px(0), // 拼图块的偏移量,单位转换为px
tfBoxBtnNeiElWidth: 0, // 用户手动触发的拼图块宽度,用于计算滑动距离
})
onMounted(() => {
console.log(props.ImagesList)
nextTick(() => {
init()
})
})
// 生成从minNum到maxNum的随机数
const randomNum = (minNum, maxNum) => {
switch (arguments.length) {
case 1:
return parseInt(Math.random() * minNum + 1, 10);
break;
case 2:
return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
break;
default:
return 0;
break;
}
}
// 关闭
const close = () => {
emit('close');
}
const init = () => {
if (!props.ImagesList.length) {
uni.showToast({
title: 'ImagesList不能为空',
icon: "none"
});
return;
}
// state.canvasX2--;
// state.canvasX = 0;
state.verifyIndex = randomNum(0, props.ImagesList.length - 1);
state.ctx = uni.createCanvasContext('tf-verify-canvas', this);
state.jgX = randomNum(150 / 2, 450 / 2);
const imageUrl = props.ImagesList[state.verifyIndex].src;
// #ifdef MP
// 在小程序执行 下载图片并绘制
uni.downloadFile({
url: imageUrl,
success: (res) => {
if (res.statusCode === 200) {
console.log(res);
uni.$u.toast("图片下载成功");
state.dqImgPath = res.tempFilePath; // 保存临时路径
huatu(); // 下载完成后开始绘制
} else {
uni.$u.toast("图片下载失败");
}
},
fail: () => {
uni.$u.toast("图片下载失败");
}
});
// #endif
// #ifndef MP
// 非此平台使用
state.dqImgPath = props.ImagesList[state.verifyIndex].src;
huatu();
// #endif
}
const endTouch = (e) => {
if (state.canvasX <= 5) {
// 滑块移动x小于5,不做任何变动。
return;
} else {
if (Math.abs(state.canvasX - state.jgX) <= 5) {
setTimeout(() => {
emit('succeed');
}, 100)
} else {
uni.$u.toast("验证失败");
resetSlider();
}
}
}
// 绘制不可移动的拼图块this.ctx, this.jgX, y, r
const drawFixedPuzzle = ({
XX,
YY,
r,
cs
}) => {
state.ctx.beginPath();
state.ctx.moveTo(-2 * r + state.jgX + cs + 2 * r, YY - 2 * r + 2 * r);
state.ctx.lineTo(-2 * r + state.jgX + cs + 5.5 * r, YY - 2 * r + 2 * r);
state.ctx.arcTo(-2 * r + state.jgX + cs + 5.5 * r, YY - 2 * r + 3 * r, XX - 2 + state.jgX * r + cs + 6.5 * r,
YY - 2 * r + 3 * r, r);
state.ctx.lineTo(-2 * r + state.jgX + cs + 7.5 * r, YY - 2 * r + 3 * r);
state.ctx.arcTo(-2 * r + state.jgX + cs + 8.5 * r, YY - 2 * r + 3 * r, -2 * r + state.jgX + cs + 8.5 * r, YY -
2 * r + 2 * r, r);
state.ctx.lineTo(-2 * r + state.jgX + cs + 12 * r, YY - 2 * r + 2 * r);
state.ctx.lineTo(-2 * r + state.jgX + cs + 12 * r, YY - 2 * r + 11 * r);
state.ctx.lineTo(-2 * r + state.jgX + cs + 8.5 * r, YY - 2 * r + 11 * r);
state.ctx.arcTo(-2 * r + state.jgX + cs + 8.5 * r, YY - 2 * r + 12 * r, -2 * r + state.jgX + cs + 7.5 * r,
YY - 2 * r + 12 * r, r);
state.ctx.lineTo(-2 * r + state.jgX + cs + 6.5 * r, YY - 2 * r + 12 * r);
state.ctx.arcTo(-2 * r + state.jgX + cs + 5.5 * r, YY - 2 * r + 12 * r, -2 * r + state.jgX + cs + 5.5 * r,
YY - 2 * r + 11 * r, r);
state.ctx.lineTo(-2 * r + state.jgX + cs + 2 * r, YY - 2 * r + 11 * r);
state.ctx.lineTo(-2 * r + state.jgX + cs + 2 * r, YY - 2 * r + 8 * r);
state.ctx.arcTo(-2 * r + state.jgX + cs + 3 * r, YY - 2 * r + 8 * r, -2 * r + state.jgX + cs + 3 * r, YY - 2 *
r + 7 * r, r);
state.ctx.lineTo(-2 * r + state.jgX + cs + 3 * r, YY - 2 * r + 6 * r);
state.ctx.arcTo(-2 * r + state.jgX + cs + 3 * r, YY - 2 * r + 5 * r, -2 * r + state.jgX + cs + 2 * r, YY - 2 *
r + 5 * r, r);
state.ctx.lineTo(-2 * r + state.jgX + cs + 2 * r, YY - 2 * r + 2 * r);
state.ctx.shadowBlur = 10;
state.ctx.shadowColor = "#ffffff";
state.ctx.fillStyle = "rgba(0,0,0,0.5)";
state.ctx.fill();
state.ctx.restore();
}
// 绘制可移动拼图块
const drawMovablePuzzle = ({
XX,
YY,
r,
cs
}) => {
state.ctx.beginPath();
state.ctx.save(); //画布区域进行保存
state.ctx.moveTo(XX - 2 * r + cs + 2 * r, YY - 2 * r + 2 * r);
state.ctx.lineTo(XX - 2 * r + cs + 5.5 * r, YY - 2 * r + 2 * r);
state.ctx.arcTo(XX - 2 * r + cs + 5.5 * r, YY - 2 * r + 3 * r, XX - 2 * r + cs + 6.5 * r, YY - 2 * r + 3 *
r, r);
state.ctx.lineTo(XX - 2 * r + cs + 7.5 * r, YY - 2 * r + 3 * r);
state.ctx.arcTo(XX - 2 * r + cs + 8.5 * r, YY - 2 * r + 3 * r, XX - 2 * r + cs + 8.5 * r, YY - 2 * r + 2 *
r, r);
state.ctx.lineTo(XX - 2 * r + cs + 12 * r, YY - 2 * r + 2 * r);
state.ctx.lineTo(XX - 2 * r + cs + 12 * r, YY - 2 * r + 11 * r);
state.ctx.lineTo(XX - 2 * r + cs + 8.5 * r, YY - 2 * r + 11 * r);
state.ctx.arcTo(XX - 2 * r + cs + 8.5 * r, YY - 2 * r + 12 * r, XX - 2 * r + cs + 7.5 * r, YY - 2 * r + 12 *
r, r);
state.ctx.lineTo(XX - 2 * r + cs + 6.5 * r, YY - 2 * r + 12 * r);
state.ctx.arcTo(XX - 2 * r + cs + 5.5 * r, YY - 2 * r + 12 * r, XX - 2 * r + cs + 5.5 * r, YY - 2 * r + 11 *
r, r);
state.ctx.lineTo(XX - 2 * r + cs + 2 * r, YY - 2 * r + 11 * r);
state.ctx.lineTo(XX - 2 * r + cs + 2 * r, YY - 2 * r + 8 * r);
state.ctx.arcTo(XX - 2 * r + cs + 3 * r, YY - 2 * r + 8 * r, XX - 2 * r + cs + 3 * r, YY - 2 * r + 7 * r,
r);
state.ctx.lineTo(XX - 2 * r + cs + 3 * r, YY - 2 * r + 6 * r);
state.ctx.arcTo(XX - 2 * r + cs + 3 * r, YY - 2 * r + 5 * r, XX - 2 * r + cs + 2 * r, YY - 2 * r + 5 * r,
r);
state.ctx.lineTo(XX - 2 * r + cs + 2 * r, YY - 2 * r + 2 * r);
state.ctx.shadowBlur = 10;
state.ctx.shadowColor = "#ffffff";
state.ctx.fill();
state.ctx.clip(); //从原始画布中剪切任意形状和尺寸
state.ctx.restore(); //进行恢复 绘画
// 根据canvas上的滑块,生成手动滑块的宽度
state.tfBoxBtnNeiElWidth = (XX - 2 * r + cs + 12 * r) - (XX - 2 * r + cs + 2 * r);
}
// 主绘制逻辑
const huatu = () => {
let XX = state.canvasX;
let obj = {
r: state.RR,
cs: state.CS,
XX,
YY: state.YY
}
// 清空画布,重新绘制
state.ctx.clearRect(0, 0, state.canvasW, state.canvasH);
// 绘制背景图
state.ctx.drawImage(state.dqImgPath, 0, 0, state.canvasW, state.canvasH);
// 绘制固定拼图块
drawFixedPuzzle(obj);
// 绘制可移动拼图块
drawMovablePuzzle(obj);
// 绘制最终图像
state.ctx.draw();
}
const resetSlider = () => {
state.canvasX = 0; // 重置实时滑块位置
state.canvasX2 = canvasX2.value; // 重置滑块初始位置
nextTick(() => {
state.canvasX2 = 0; // 重置滑块初始位置
init(); // 延迟调用重新绘制拼图
})
}
const changePath = (e) => {
// #ifdef H5
state.canvasX = e.detail.x;
canvasX2.value = e.detail.x;
// #endif
// #ifndef H5
state.canvasX = e.target.x;
canvasX2.value = e.target.x;
// #endif
// console.log(canvasX2.value);
huatu();
}
</script>
<style scoped lang="scss">
view,
text {
line-height: normal;
}
.tf-Box-Bg {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 100;
background-color: rgba(0, 0, 0, .3);
display: flex;
align-items: center;
justify-content: center;
.tf-Box {
width: 680rpx;
min-height: 584rpx;
background-color: #fff;
border-radius: 32rpx;
box-shadow: 0 0 50rpx 0rpx rgba(0, 0, 0, .2);
.tf-Box-title {
height: 100rpx;
line-height: 100rpx;
padding: 0 32rpx;
border-bottom: 1px solid #E1E3E9;
flex-direction: row;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 500;
font-size: 32rpx;
color: #111111;
.text {
font-size: 32rpx;
flex: 1;
}
.tf-close {
width: 28rpx;
height: 28rpx;
}
}
.tf-Box-center {
margin-top: 30rpx;
margin-bottom: 30rpx;
align-self: center;
}
.tf-Box-BtnBox {
margin-top: 30rpx;
margin-bottom: 30rpx;
align-self: center;
width: 580rpx;
height: 75rpx;
line-height: 75rpx;
text-align: center;
font-size: 28rpx;
border-radius: 6rpx;
background-color: #F7F8F9;
overflow: hidden;
position: relative;
border: 2rpx solid #f3f3f3;
.tf-Box-BtnBox-text {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
color: #424649;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.tf-Box-BtnNei {
height: 100%;
width: 88rpx;
background-color: #ffffff;
box-shadow: 0 0 10rpx 0rpx rgba(0, 0, 0, .2);
background-image: url(https://ggyx-img.oss-cn-zhangjiakou.aliyuncs.com/ggyx_uploads/20250726/293363f9e00350374eca61b61e14b821tf-arrows.png);
background-size: 34rpx;
background-position: center;
background-repeat: no-repeat;
.tf-Box-BtnNei-leftBox {
position: absolute;
top: 0;
height: 100%;
left: -580rpx;
width: 580rpx;
}
}
}
}
}
.tf-content {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
</style>
浮世 (作者)
没有
2025-07-29 16:48