效果:点击后提示验证成功

代码:

下载依赖:react-draggable、lucide-react;

index.tsx
import { useState } from 'react';
import SliderCaptcha from '../components/SliderCaptcha';

const Index = () => {
  const [showCaptcha, setShowCaptcha] = useState(false);

  const handleSuccess = () => {
    setTimeout(() => {
      setShowCaptcha(false);
      alert('验证成功!');
    }, 500);
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <div className="text-center">
        <h1 className="text-4xl font-bold mb-4">滑块验证示例</h1>
        <button
          onClick={() => setShowCaptcha(true)}
          className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
        >
          点击验证
        </button>

        {showCaptcha && (
          <SliderCaptcha
            onClose={() => setShowCaptcha(false)}
            onSuccess={handleSuccess}
          />
        )}
      </div>
    </div>
  );
};

export default Index;

SliderCaptcha.tsx
import { ArrowLeftRight, X, Check } from 'lucide-react';
import { useRef, useEffect, memo, useState } from 'react';
import { useImageCrop } from '../utils/imageUtils';
import Draggable from 'react-draggable';

const PuzzlePiece = memo(({ croppedPuzzle, position, isDragging }) => (
  <div 
    className={`absolute top-[60px] w-[40px] h-[40px] cursor-pointer overflow-hidden transition-transform duration-0
      ${isDragging ? 'shadow-lg' : ''}`}
    style={{
      border: '1px solid rgba(255,255,255,0.3)',
      boxShadow: isDragging ? '0 0 10px rgba(0,0,0,0.3)' : 'none',
      left: 0,
      transform: `translateX(${position.x}px)`
    }}
  >
    {croppedPuzzle && (
      <img 
        src={croppedPuzzle} 
        alt="拼图块"
        className="w-full h-full"
        draggable={false}
      />
    )}
  </div>
));

const SliderHandle = memo(({ position, isSuccess, isDragging }) => (
  <div 
    className={`absolute top-1 h-10 w-10 rounded shadow cursor-pointer
      flex items-center justify-center transition-colors duration-300
      ${isDragging ? 'shadow-lg' : ''}
      ${isSuccess ? 'bg-green-500 text-white' : 'bg-white'}`}
    style={{
      willChange: 'transform'
    }}
  >
    {isSuccess ? (
      <Check className="h-5 w-5" />
    ) : (
      <ArrowLeftRight className="h-5 w-5 text-gray-600" />
    )}
  </div>
));

const SliderCaptcha = ({ onClose, onSuccess }) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [isDragging, setIsDragging] = useState(false);
  const [isSuccess, setIsSuccess] = useState(false);
  const containerRef = useRef(null);
  const sliderRef = useRef(null);
  
  const puzzleWidth = 40;
  const puzzleHeight = 40;
  const targetPosition = 200;
  const tolerance = 10;

  const backgroundImage = "https://nocode.meituan.com/photo/search?keyword=canyon&width=320&height=160&source=meituan";
  const croppedPuzzle = useImageCrop(backgroundImage, targetPosition, 60, puzzleWidth, puzzleHeight);

  const handleDrag = (e, data) => {
    const newX = Math.max(0, data.x);
    console.log('newX', newX, data.x)
    setPosition({ x: newX, y: 0 });
    setIsDragging(true);
  };

  const handleDragStop = (e, data) => {
    setIsDragging(false);
    const currentPosition = Math.max(0, data.x);
    
    if (Math.abs(currentPosition - targetPosition) <= tolerance) {
      setIsSuccess(true);
      setPosition({ x: targetPosition, y: 0 });
      onSuccess?.();
    } else {
      setPosition({ x: 0, y: 0 });
    }
  };

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
      <div className="bg-white rounded-lg p-4 w-[320px]" ref={containerRef}>
        <div className="flex justify-between items-center mb-4">
          <h3 className="text-lg font-medium">安全验证</h3>
          <button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
            <X className="h-5 w-5" />
          </button>
        </div>
        
        <div className="relative mb-4 h-[160px] bg-gray-100 overflow-hidden">
          <img 
            src={backgroundImage}
            className="w-full h-full object-cover"
            alt="验证背景"
            draggable={false}
          />
          
          <div 
            className="absolute top-[60px] left-[200px] w-[40px] h-[40px] bg-black/40 backdrop-blur-[1px]"
            style={{
              boxShadow: 'inset 0 0 10px rgba(0,0,0,0.3)',
              border: '1px solid rgba(255,255,255,0.2)'
            }}
          />

          <PuzzlePiece 
            croppedPuzzle={croppedPuzzle} 
            position={position}
            isDragging={isDragging} 
          />
        </div>

        <div className="relative h-12 bg-gray-100 rounded" ref={sliderRef}>
          <Draggable
            axis="x"
            position={position}
            onDrag={handleDrag}
            onStop={handleDragStop}
            bounds={{left: 0, right: 280}}
            nodeRef={sliderRef}
          >
            <div className="absolute top-0 left-0 w-10 h-full cursor-pointer">
              <SliderHandle 
                position={position}
                isSuccess={isSuccess} 
                isDragging={isDragging} 
              />
            </div>
          </Draggable>
          <div className="h-full flex items-center justify-center text-gray-400">
            {isSuccess ? '验证通过' : '拖动下方滑块完成拼图'}
          </div>
        </div>
      </div>
    </div>
  );
};

export default SliderCaptcha;
useImageCrop.tsx
import { useEffect, useState, useCallback } from 'react';

export const useImageCrop = (imageUrl, x, y, width, height) => {
  const [croppedImage, setCroppedImage] = useState(null);

  const cropImage = useCallback(() => {
    const img = new Image();
    img.crossOrigin = "anonymous";
    
    img.onload = () => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d', { alpha: false });
      
      canvas.width = width;
      canvas.height = height;
      
      ctx.imageSmoothingEnabled = false;
      ctx.drawImage(img, x, y, width, height, 0, 0, width, height);
      
      const dataUrl = canvas.toDataURL('image/png', 0.8);
      setCroppedImage(dataUrl);
    };

    img.src = imageUrl;
  }, [imageUrl, x, y, width, height]);

  useEffect(() => {
    cropImage();
  }, [cropImage]);

  return croppedImage;
};

更多推荐