feat: 增加基于图片拼图切片平移的验证码

pull/168/head^2
chenweiran 2025-07-03 18:20:20 +08:00
parent 8554924cb9
commit bbd8a53d9d
6 changed files with 64 additions and 3 deletions

View File

@ -3,4 +3,5 @@ export { default as PointSelectionCaptchaCard } from './point-selection-captcha/
export { default as SliderCaptcha } from './slider-captcha/index.vue'; export { default as SliderCaptcha } from './slider-captcha/index.vue';
export { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue'; export { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue';
export { default as SliderTranslateCaptcha } from './slider-translate-captcha/index.vue';
export type * from './types'; export type * from './types';

View File

@ -127,7 +127,12 @@ function resetCanvas() {
if (!puzzleCanvas || !pieceCanvas) return; if (!puzzleCanvas || !pieceCanvas) return;
pieceCanvas.width = canvasWidth; pieceCanvas.width = canvasWidth;
const puzzleCanvasCtx = puzzleCanvas.getContext('2d'); const puzzleCanvasCtx = puzzleCanvas.getContext('2d');
const pieceCanvasCtx = pieceCanvas.getContext('2d'); // Canvas2D: Multiple readback operations using getImageData
// are faster with the willReadFrequently attribute set to true.
// See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently (anonymous)
const pieceCanvasCtx = pieceCanvas.getContext('2d', {
willReadFrequently: true,
});
if (!puzzleCanvasCtx || !pieceCanvasCtx) return; if (!puzzleCanvasCtx || !pieceCanvasCtx) return;
puzzleCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight); puzzleCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);
pieceCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight); pieceCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);
@ -139,9 +144,16 @@ function initCanvas() {
const pieceCanvas = unref(pieceCanvasRef); const pieceCanvas = unref(pieceCanvasRef);
if (!puzzleCanvas || !pieceCanvas) return; if (!puzzleCanvas || !pieceCanvas) return;
const puzzleCanvasCtx = puzzleCanvas.getContext('2d'); const puzzleCanvasCtx = puzzleCanvas.getContext('2d');
const pieceCanvasCtx = pieceCanvas.getContext('2d'); // Canvas2D: Multiple readback operations using getImageData
// are faster with the willReadFrequently attribute set to true.
// See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently (anonymous)
const pieceCanvasCtx = pieceCanvas.getContext('2d', {
willReadFrequently: true,
});
if (!puzzleCanvasCtx || !pieceCanvasCtx) return; if (!puzzleCanvasCtx || !pieceCanvasCtx) return;
const img = new Image(); const img = new Image();
//
img.crossOrigin = 'Anonymous';
img.src = src; img.src = src;
img.addEventListener('load', () => { img.addEventListener('load', () => {
draw(puzzleCanvasCtx, pieceCanvasCtx); draw(puzzleCanvasCtx, pieceCanvasCtx);
@ -158,6 +170,7 @@ function initCanvas() {
); );
pieceCanvas.width = pieceLength; pieceCanvas.width = pieceLength;
pieceCanvasCtx.putImageData(imageData, 0, sy); pieceCanvasCtx.putImageData(imageData, 0, sy);
setLeft('0');
}); });
} }
@ -265,7 +278,7 @@ onMounted(() => {
@click="resume" @click="resume"
></canvas> ></canvas>
<div <div
class="absolute bottom-3 left-0 z-10 block h-7 w-full text-center text-xs leading-[30px] text-white" class="h-15 absolute bottom-3 left-0 z-10 block w-full text-center text-xs leading-[30px] text-white"
> >
<div <div
v-if="state.showTip" v-if="state.showTip"

View File

@ -159,6 +159,42 @@ export interface SliderRotateCaptchaProps {
defaultTip?: string; defaultTip?: string;
} }
export interface SliderTranslateCaptchaProps {
/**
* @description
* @default 420
*/
canvasWidth?: number;
/**
* @description
* @default 280
*/
canvasHeight?: number;
/**
* @description
* @default 42
*/
squareLength?: number;
/**
* @description
* @default 10
*/
circleRadius?: number;
/**
* @description
*/
src?: string;
/**
* @description
* @default 3
*/
diffDistance?: number;
/**
* @description
*/
defaultTip?: string;
}
export interface CaptchaVerifyPassingData { export interface CaptchaVerifyPassingData {
isPassing: boolean; isPassing: boolean;
time: number | string; time: number | string;

View File

@ -41,6 +41,7 @@
"pointSelection": "Point Selection Captcha", "pointSelection": "Point Selection Captcha",
"sliderCaptcha": "Slider Captcha", "sliderCaptcha": "Slider Captcha",
"sliderRotateCaptcha": "Rotate Captcha", "sliderRotateCaptcha": "Rotate Captcha",
"sliderTranslateCaptcha": "Translate Captcha",
"captchaCardTitle": "Please complete the security verification", "captchaCardTitle": "Please complete the security verification",
"pageDescription": "Verify user identity by clicking on specific locations in the image.", "pageDescription": "Verify user identity by clicking on specific locations in the image.",
"pageTitle": "Captcha Component Example", "pageTitle": "Captcha Component Example",

View File

@ -44,6 +44,7 @@
"pointSelection": "点选验证", "pointSelection": "点选验证",
"sliderCaptcha": "滑块验证", "sliderCaptcha": "滑块验证",
"sliderRotateCaptcha": "旋转验证", "sliderRotateCaptcha": "旋转验证",
"sliderTranslateCaptcha": "拼图滑块验证",
"captchaCardTitle": "请完成安全验证", "captchaCardTitle": "请完成安全验证",
"pageDescription": "通过点击图片中的特定位置来验证用户身份。", "pageDescription": "通过点击图片中的特定位置来验证用户身份。",
"pageTitle": "验证码组件示例", "pageTitle": "验证码组件示例",

View File

@ -196,6 +196,15 @@ const routes: RouteRecordRaw[] = [
title: $t('examples.captcha.sliderRotateCaptcha'), title: $t('examples.captcha.sliderRotateCaptcha'),
}, },
}, },
{
name: 'TranslateVerifyExample',
path: '/examples/captcha/slider-translate',
component: () =>
import('#/views/examples/captcha/slider-translate-captcha.vue'),
meta: {
title: $t('examples.captcha.sliderTranslateCaptcha'),
},
},
{ {
name: 'CaptchaPointSelectionExample', name: 'CaptchaPointSelectionExample',
path: '/examples/captcha/point-selection', path: '/examples/captcha/point-selection',