# 滑块验证码 API 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为小雨开发工具箱创建一个纯前端的滑块验证码组件和模拟 API,包含验证码生成、滑块校验、刷新重置等功能,可直接集成到登录/注册流程中。 **Architecture:** 采用纯前端实现方案:使用 HTML5 Canvas 生成拼图验证码图片(底图 + 缺口 + 滑块),通过 React 组件管理交互状态。模拟后端 API 使用本地计算验证滑块位置偏移量。组件提供完整的生命周期:生成验证码 -> 用户拖拽 -> 校验结果 -> 成功回调/失败重置。 **Tech Stack:** React 18, TypeScript, Vite, Ant Design, HTML5 Canvas API --- ## 文件结构 | 文件 | 操作 | 说明 | |------|------|------| | `src/components/SliderCaptcha/SliderCaptcha.tsx` | 创建 | 滑块验证码主组件 | | `src/components/SliderCaptcha/SliderCaptcha.css` | 创建 | 滑块验证码样式 | | `src/components/SliderCaptcha/index.ts` | 创建 | 组件导出入口 | | `src/components/SliderCaptcha/captchaUtils.ts` | 创建 | Canvas 生成与验证工具函数 | | `src/pages/SliderCaptchaDemo/SliderCaptchaDemo.tsx` | 创建 | 演示页面(展示组件用法) | | `src/pages/SliderCaptchaDemo/SliderCaptchaDemo.css` | 创建 | 演示页面样式 | | `src/App.tsx` | 修改 | 添加演示页面路由 | | `src/pages/Utility/Utility.tsx` | 修改 | 在工具列表中注册滑块验证码 | --- ## 设计规范 - **组件尺寸:** 默认宽度 320px,高度自适应(图片区 + 滑块轨道区) - **图片尺寸:** 底图 320x160px,缺口大小 40x40px - **滑块轨道:** 高度 40px,圆角 20px,背景色 #e8ecf1 - **滑块按钮:** 40x40px 正方形,圆角 4px,主色渐变背景 - **颜色主题:** 成功绿色 `#22c55e`,失败红色 `#ef4444`,主色 `#3b82f6` - **动画效果:** 滑块拖拽跟随、验证成功/失败抖动动画、刷新旋转动画 - **响应式:** 支持通过 props 自定义宽度 --- ## 组件接口设计 ```ts interface SliderCaptchaProps { width?: number; // 验证码宽度,默认 320 height?: number; // 图片高度,默认 160 gapSize?: number; // 缺口大小,默认 40 tolerance?: number; // 容错像素,默认 5 onSuccess?: () => void; // 验证成功回调 onFail?: () => void; // 验证失败回调 onRefresh?: () => void; // 刷新回调 className?: string; // 自定义类名 style?: React.CSSProperties; // 自定义样式 } ``` --- ## Task 1: 创建 Canvas 验证码工具函数 **Files:** - Create: `src/components/SliderCaptcha/captchaUtils.ts` ### 功能需求 1. **生成随机缺口位置**:在图片区域内随机生成缺口坐标 (x, y) 2. **绘制底图**:在 Canvas 上绘制背景图,并在缺口位置绘制半透明遮罩 3. **绘制滑块**:提取缺口区域的图像作为滑块,绘制到单独的 Canvas 4. **验证位置**:比对用户滑动的位置与缺口位置,计算偏移量是否在容错范围内 5. **生成干扰线**:在底图上绘制随机干扰线增加识别难度 - [ ] **Step 1: 创建 captchaUtils.ts** ```ts /** * 滑块验证码工具函数 * 纯前端实现,使用 Canvas 生成拼图验证码 */ export interface GapPosition { x: number; y: number; } export interface CaptchaResult { bgCanvas: HTMLCanvasElement; // 背景图(带缺口遮罩) sliderCanvas: HTMLCanvasElement; // 滑块图 gapX: number; // 缺口 X 坐标(正确答案) gapY: number; // 缺口 Y 坐标 } /** * 生成随机整数 [min, max] */ export const randomInt = (min: number, max: number): number => { return Math.floor(Math.random() * (max - min + 1)) + min; }; /** * 生成随机颜色 */ export const randomColor = (min = 0, max = 255): string => { const r = randomInt(min, max); const g = randomInt(min, max); const b = randomInt(min, max); return `rgb(${r}, ${g}, ${b})`; }; /** * 生成随机缺口位置 */ export const generateGapPosition = ( width: number, height: number, gapSize: number ): GapPosition => { const padding = gapSize + 10; return { x: randomInt(padding, width - padding), y: randomInt(padding, height - padding), }; }; /** * 绘制圆角矩形路径 */ export const drawRoundRectPath = ( ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number ) => { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); }; /** * 绘制缺口形状(带凸起的拼图块) */ export const drawGapShape = ( ctx: CanvasRenderingContext2D, x: number, y: number, size: number ) => { const r = size / 2; const bulgeR = size / 3; ctx.beginPath(); ctx.moveTo(x, y + r); // 上边 - 带凸起 ctx.lineTo(x + r - bulgeR, y); ctx.arc(x + r, y, bulgeR, Math.PI, 0, false); ctx.lineTo(x + size, y + r); // 右边 - 带凸起 ctx.lineTo(x + size, y + r - bulgeR); ctx.arc(x + size, y + r, bulgeR, -Math.PI / 2, Math.PI / 2, false); ctx.lineTo(x + size, y + size); // 下边 ctx.lineTo(x, y + size); // 左边 ctx.lineTo(x, y + r); ctx.closePath(); }; /** * 生成验证码图片 */ export const generateCaptcha = async ( imageSrc: string, width: number, height: number, gapSize: number ): Promise => { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { const gapPos = generateGapPosition(width, height, gapSize); // 创建背景 Canvas const bgCanvas = document.createElement("canvas"); bgCanvas.width = width; bgCanvas.height = height; const bgCtx = bgCanvas.getContext("2d")!; // 绘制背景图 bgCtx.drawImage(img, 0, 0, width, height); // 绘制缺口遮罩 bgCtx.save(); bgCtx.globalCompositeOperation = "destination-out"; drawGapShape(bgCtx, gapPos.x, gapPos.y, gapSize); bgCtx.fill(); bgCtx.restore(); // 绘制缺口边框和高亮 bgCtx.save(); bgCtx.strokeStyle = "rgba(255, 255, 255, 0.6)"; bgCtx.lineWidth = 2; bgCtx.shadowColor = "rgba(0, 0, 0, 0.3)"; bgCtx.shadowBlur = 5; drawGapShape(bgCtx, gapPos.x, gapPos.y, gapSize); bgCtx.stroke(); bgCtx.restore(); // 绘制干扰线 for (let i = 0; i < 5; i++) { bgCtx.beginPath(); bgCtx.moveTo(randomInt(0, width), randomInt(0, height)); bgCtx.lineTo(randomInt(0, width), randomInt(0, height)); bgCtx.strokeStyle = randomColor(100, 200); bgCtx.lineWidth = randomInt(1, 3); bgCtx.stroke(); } // 创建滑块 Canvas const sliderCanvas = document.createElement("canvas"); sliderCanvas.width = gapSize; sliderCanvas.height = height; const sliderCtx = sliderCanvas.getContext("2d")!; // 绘制滑块形状(从原图裁剪) sliderCtx.save(); drawGapShape(sliderCtx, 0, gapPos.y, gapSize); sliderCtx.clip(); sliderCtx.drawImage(img, 0, 0, width, height); sliderCtx.restore(); // 绘制滑块边框 sliderCtx.save(); sliderCtx.strokeStyle = "rgba(255, 255, 255, 0.8)"; sliderCtx.lineWidth = 2; sliderCtx.shadowColor = "rgba(0, 0, 0, 0.4)"; sliderCtx.shadowBlur = 8; drawGapShape(sliderCtx, 0, gapPos.y, gapSize); sliderCtx.stroke(); sliderCtx.restore(); resolve({ bgCanvas, sliderCanvas, gapX: gapPos.x, gapY: gapPos.y, }); }; img.onerror = () => reject(new Error("图片加载失败")); img.src = imageSrc; }); }; /** * 验证滑块位置 */ export const verifyCaptcha = ( userX: number, gapX: number, tolerance: number = 5 ): boolean => { return Math.abs(userX - gapX) <= tolerance; }; /** * 获取随机图片 URL(使用 picsum 或本地图片) */ export const getRandomImage = (width: number, height: number): string => { const seed = randomInt(1, 1000); return `https://picsum.photos/seed/${seed}/${width}/${height}`; }; ``` --- ## Task 2: 创建滑块验证码主组件 **Files:** - Create: `src/components/SliderCaptcha/SliderCaptcha.tsx` - Create: `src/components/SliderCaptcha/SliderCaptcha.css` ### 功能需求 1. **状态管理**:加载中、就绪、拖拽中、验证中、成功、失败 六种状态 2. **拖拽交互**:鼠标/触摸按下 -> 拖拽移动 -> 释放,滑块跟随手指移动 3. **边界限制**:滑块只能在轨道范围内移动 4. **验证反馈**:释放后计算位置,显示成功(绿色)或失败(红色)动画 5. **自动刷新**:验证失败后自动重置,成功后显示成功状态 6. **手动刷新**:点击刷新按钮重新生成验证码 - [ ] **Step 2: 创建 SliderCaptcha.tsx** ```tsx import "./SliderCaptcha.css"; import React, { useState, useRef, useEffect, useCallback } from "react"; import { LoadingOutlined, ReloadOutlined, CheckCircleOutlined, CloseCircleOutlined } from "@ant-design/icons"; import { generateCaptcha, verifyCaptcha, getRandomImage, CaptchaResult, } from "./captchaUtils"; export interface SliderCaptchaProps { width?: number; height?: number; gapSize?: number; tolerance?: number; onSuccess?: () => void; onFail?: () => void; onRefresh?: () => void; className?: string; style?: React.CSSProperties; } type CaptchaStatus = "loading" | "ready" | "dragging" | "verifying" | "success" | "fail"; const SliderCaptcha: React.FC = ({ width = 320, height = 160, gapSize = 40, tolerance = 5, onSuccess, onFail, onRefresh, className = "", style = {}, }) => { const [status, setStatus] = useState("loading"); const [captchaData, setCaptchaData] = useState(null); const [sliderX, setSliderX] = useState(0); const [message, setMessage] = useState("向右滑动完成验证"); const containerRef = useRef(null); const trackRef = useRef(null); const isDraggingRef = useRef(false); const startXRef = useRef(0); const startSliderXRef = useRef(0); const maxSliderX = width - gapSize; const loadCaptcha = useCallback(async () => { setStatus("loading"); setSliderX(0); setMessage("向右滑动完成验证"); try { const imageSrc = getRandomImage(width, height); const data = await generateCaptcha(imageSrc, width, height, gapSize); setCaptchaData(data); setStatus("ready"); } catch { setMessage("图片加载失败,请刷新重试"); setStatus("fail"); } }, [width, height, gapSize]); useEffect(() => { loadCaptcha(); }, [loadCaptcha]); const handleRefresh = () => { loadCaptcha(); onRefresh?.(); }; const getSliderXFromEvent = (clientX: number): number => { const track = trackRef.current; if (!track) return 0; const rect = track.getBoundingClientRect(); const x = clientX - rect.left - gapSize / 2; return Math.max(0, Math.min(x, maxSliderX)); }; const handleMouseDown = (e: React.MouseEvent) => { if (status !== "ready" && status !== "fail") return; isDraggingRef.current = true; startXRef.current = e.clientX; startSliderXRef.current = sliderX; setStatus("dragging"); setMessage(""); }; const handleTouchStart = (e: React.TouchEvent) => { if (status !== "ready" && status !== "fail") return; isDraggingRef.current = true; startXRef.current = e.touches[0].clientX; startSliderXRef.current = sliderX; setStatus("dragging"); setMessage(""); }; const handleMouseMove = useCallback( (e: MouseEvent) => { if (!isDraggingRef.current) return; const deltaX = e.clientX - startXRef.current; const newX = Math.max(0, Math.min(startSliderXRef.current + deltaX, maxSliderX)); setSliderX(newX); }, [maxSliderX] ); const handleTouchMove = useCallback( (e: TouchEvent) => { if (!isDraggingRef.current) return; const deltaX = e.touches[0].clientX - startXRef.current; const newX = Math.max(0, Math.min(startSliderXRef.current + deltaX, maxSliderX)); setSliderX(newX); }, [maxSliderX] ); const doVerify = useCallback(() => { if (!isDraggingRef.current || !captchaData) return; isDraggingRef.current = false; setStatus("verifying"); const isValid = verifyCaptcha(sliderX, captchaData.gapX, tolerance); if (isValid) { setStatus("success"); setMessage("验证成功"); onSuccess?.(); } else { setStatus("fail"); setMessage("验证失败,请重试"); setSliderX(0); onFail?.(); setTimeout(() => { handleRefresh(); }, 1000); } }, [captchaData, sliderX, tolerance, onSuccess, onFail]); const handleMouseUp = useCallback(() => { doVerify(); }, [doVerify]); const handleTouchEnd = useCallback(() => { doVerify(); }, [doVerify]); useEffect(() => { window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); window.addEventListener("touchmove", handleTouchMove); window.addEventListener("touchend", handleTouchEnd); return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); window.removeEventListener("touchmove", handleTouchMove); window.removeEventListener("touchend", handleTouchEnd); }; }, [handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]); const getStatusColor = () => { switch (status) { case "success": return "#22c55e"; case "fail": return "#ef4444"; default: return "#3b82f6"; } }; const getStatusIcon = () => { switch (status) { case "success": return ; case "fail": return ; default: return null; } }; return (
{/* 图片区域 */}
{status === "loading" && (
加载中...
)} {captchaData && ( <> {/* 背景图 */} captcha background {/* 滑块图 */}
slider
{/* 刷新按钮 */} )} {/* 状态遮罩 */} {(status === "success" || status === "fail") && (
{getStatusIcon()} {message}
)}
{/* 滑块轨道 */}
{/* 轨道背景文字 */}
{message}
{/* 进度条 */}
{/* 滑块按钮 */}
{status === "loading" ? ( ) : status === "success" ? ( ) : status === "fail" ? ( ) : ( → )}
); }; export default SliderCaptcha; ``` - [ ] **Step 3: 创建 SliderCaptcha.css** ```css .slider-captcha { user-select: none; -webkit-user-select: none; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } /* 图片区域 */ .sc-image-area { position: relative; border-radius: 8px 8px 0 0; overflow: hidden; background: #f1f5f9; } .sc-bg-image { display: block; border-radius: 8px 8px 0 0; } .sc-slider-piece { position: absolute; top: 0; left: 0; transition: transform 0.1s ease-out; pointer-events: none; } .sc-slider-piece img { display: block; } .sc-refresh-btn { position: absolute; top: 8px; right: 8px; width: 32px; height: 32px; border: none; border-radius: 6px; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(4px); color: #64748b; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; z-index: 10; } .sc-refresh-btn:hover { background: #fff; color: #3b82f6; transform: rotate(180deg); } .sc-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; width: 100%; height: 100%; color: #94a3b8; font-size: 14px; } /* 状态遮罩 */ .sc-status-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; animation: scFadeIn 0.3s ease; z-index: 5; } .sc-status-overlay.success { animation: scSuccessPulse 0.5s ease; } .sc-status-overlay.fail { animation: scShake 0.5s ease; } .sc-status-content { display: flex; align-items: center; gap: 8px; padding: 10px 20px; background: rgba(255, 255, 255, 0.95); border-radius: 20px; font-size: 14px; font-weight: 600; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .sc-status-content .anticon { font-size: 18px; } /* 滑块轨道 */ .sc-track { position: relative; background: #e8ecf1; border-radius: 0 0 8px 8px; overflow: hidden; } .sc-track-text { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 500; transition: color 0.3s ease; z-index: 1; } .sc-track-progress { position: absolute; top: 0; left: 0; height: 100%; border-radius: 0 0 0 8px; transition: width 0.1s ease-out, opacity 0.3s ease; z-index: 0; } .sc-slider-btn { position: absolute; top: 0; left: 0; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 16px; transition: transform 0.1s ease-out, background-color 0.3s ease; z-index: 2; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } .sc-slider-btn:active { cursor: grabbing !important; } .sc-slider-btn.success, .sc-slider-btn.fail { cursor: default !important; } .sc-arrow { font-size: 18px; font-weight: bold; } /* 动画 */ @keyframes scFadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes scSuccessPulse { 0% { transform: scale(1); } 50% { transform: scale(1.02); } 100% { transform: scale(1); } } @keyframes scShake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-8px); } 40% { transform: translateX(8px); } 60% { transform: translateX(-4px); } 80% { transform: translateX(4px); } } ``` --- ## Task 3: 创建组件导出入口 **Files:** - Create: `src/components/SliderCaptcha/index.ts` - [ ] **Step 4: 创建 index.ts** ```ts export { default as SliderCaptcha } from "./SliderCaptcha"; export type { SliderCaptchaProps } from "./SliderCaptcha"; export * from "./captchaUtils"; ``` --- ## Task 4: 创建演示页面 **Files:** - Create: `src/pages/SliderCaptchaDemo/SliderCaptchaDemo.tsx` - Create: `src/pages/SliderCaptchaDemo/SliderCaptchaDemo.css` ### 功能需求 1. **展示组件**:居中展示滑块验证码组件 2. **状态反馈**:显示验证成功/失败的状态提示 3. **参数调节**:提供宽度、高度、缺口大小、容错值的调节滑块 4. **代码示例**:展示组件的使用代码 5. **集成说明**:说明如何集成到登录/注册流程 - [ ] **Step 5: 创建 SliderCaptchaDemo.tsx** ```tsx import "./SliderCaptchaDemo.css"; import React, { useState } from "react"; import { Button, Slider, message, Card } from "antd"; import { ArrowLeftOutlined, SafetyOutlined, CheckCircleOutlined, InfoCircleOutlined, } from "@ant-design/icons"; import { useNavigate } from "react-router-dom"; import { useRecordHistory } from "@/hooks/useRecordHistory"; import { SliderCaptcha } from "@/components/SliderCaptcha"; const SliderCaptchaDemo: React.FC = () => { const navigate = useNavigate(); useRecordHistory({ type: "tool", title: "滑块验证码", description: "拼图滑块验证码组件,支持拖拽验证", category: "实用", link: "/utility/slider-captcha", }); const [width, setWidth] = useState(320); const [height, setHeight] = useState(160); const [gapSize, setGapSize] = useState(40); const [tolerance, setTolerance] = useState(5); const [verifyResult, setVerifyResult] = useState(""); const [key, setKey] = useState(0); const handleSuccess = () => { setVerifyResult("success"); message.success("验证成功!"); }; const handleFail = () => { setVerifyResult("fail"); message.error("验证失败,请重试"); }; const handleRefresh = () => { setVerifyResult(""); }; const resetComponent = () => { setKey((k) => k + 1); setVerifyResult(""); }; const codeExample = ` console.log("验证成功")} onFail={() => console.log("验证失败")} />`; return (
{Array.from({ length: 6 }).map((_, i) => (
))}

滑块验证码

{/* 左侧:验证码组件 */}
{verifyResult && (
{verifyResult === "success" ? ( <> 验证通过!您可以继续操作。 ) : ( <> 验证未通过,组件已自动刷新。 )}
)}
{/* 右侧:参数调节 */}
{codeExample}

1. 导入组件

{`import { SliderCaptcha } from "@/components/SliderCaptcha";`}

2. 在表单中使用

{` setCaptchaVerified(true)}
  onFail={() => setCaptchaVerified(false)}
/>`}

3. 表单提交校验

{`if (!captchaVerified) {
  message.error("请先完成滑块验证");
  return;
}`}
🔒

安全验证

滑块验证码有效防止机器人攻击,比传统图形验证码用户体验更好。

⚡

纯前端实现

无需后端支持,所有验证逻辑在浏览器本地完成,响应速度快。

📱

多端适配

支持鼠标拖拽和触摸滑动,完美适配桌面端和移动端。

); }; export default SliderCaptchaDemo; ``` - [ ] **Step 6: 创建 SliderCaptchaDemo.css** ```css .slider-captcha-demo-page { min-height: 100vh; background: #f0f2f5; } .slider-captcha-demo-hero { min-height: 200px; background: linear-gradient(135deg, #10b981 0%, #3b82f6 50%, #8b5cf6 100%); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; padding: 40px 20px; } .slider-captcha-demo-hero::before { content: ""; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle at 30% 50%, rgba(255, 255, 255, 0.1) 0%, transparent 50%), radial-gradient(circle at 70% 80%, rgba(255, 255, 255, 0.08) 0%, transparent 40%); animation: sccHeroLight 8s ease-in-out infinite alternate; } @keyframes sccHeroLight { 0% { transform: translate(0, 0); } 100% { transform: translate(-20px, -10px); } } .slider-captcha-demo-hero-particles { position: absolute; inset: 0; pointer-events: none; overflow: hidden; } .scc-particle { position: absolute; border-radius: 50%; background: rgba(255, 255, 255, 0.12); animation: sccFloat 6s ease-in-out infinite; } .scc-particle-1 { width: 50px; height: 50px; top: 15%; left: 8%; animation-delay: 0s; } .scc-particle-2 { width: 35px; height: 35px; top: 65%; left: 18%; animation-delay: 1s; } .scc-particle-3 { width: 65px; height: 65px; top: 25%; right: 12%; animation-delay: 2s; } .scc-particle-4 { width: 25px; height: 25px; top: 70%; right: 25%; animation-delay: 0.5s; } .scc-particle-5 { width: 40px; height: 40px; top: 45%; left: 45%; animation-delay: 1.5s; } .scc-particle-6 { width: 30px; height: 30px; bottom: 20%; left: 30%; animation-delay: 3s; } @keyframes sccFloat { 0%, 100% { transform: translateY(0) scale(1); opacity: 0.3; } 50% { transform: translateY(-18px) scale(1.1); opacity: 0.6; } } .slider-captcha-demo-hero-content { text-align: center; position: relative; z-index: 1; } .slider-captcha-demo-hero-icon { width: 64px; height: 64px; border-radius: 20px; background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px); display: inline-flex; align-items: center; justify-content: center; font-size: 28px; color: #fff; margin-bottom: 16px; border: 1px solid rgba(255, 255, 255, 0.3); } .slider-captcha-demo-hero-title { font-size: 32px; font-weight: 800; color: #fff; margin: 0; letter-spacing: 2px; } .slider-captcha-demo-container { max-width: 900px; margin: -30px auto 0; padding: 0 20px 60px; position: relative; z-index: 2; } .slider-captcha-demo-toolbar { display: flex; align-items: center; margin-bottom: 20px; } .scc-back-btn { border-radius: 8px; font-weight: 500; color: #64748b; border-color: #e2e8f0; } .scc-back-btn:hover { color: #10b981; border-color: #10b981; } .scc-demo-main { display: grid; grid-template-columns: 1fr 360px; gap: 24px; margin-bottom: 24px; } .scc-demo-left { background: #fff; border-radius: 14px; padding: 32px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); border: 1px solid #e8ecf1; display: flex; flex-direction: column; align-items: center; min-height: 400px; } .scc-component-wrapper { display: flex; justify-content: center; } .scc-result { display: flex; align-items: center; gap: 8px; margin-top: 16px; padding: 10px 20px; border-radius: 8px; font-size: 14px; font-weight: 500; animation: sccFadeIn 0.3s ease; } .scc-result.success { background: #dcfce7; color: #16a34a; } .scc-result.fail { background: #fee2e2; color: #dc2626; } .scc-demo-right { display: flex; flex-direction: column; } .scc-param-group { margin-bottom: 16px; } .scc-param-group label { display: block; font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 6px; } .scc-code-block { background: #1e1e2e; color: #a6accd; padding: 12px; border-radius: 8px; font-size: 12px; line-height: 1.6; overflow-x: auto; margin: 0; } .scc-guide p { font-size: 13px; font-weight: 600; color: #334155; margin: 12px 0 6px; } .scc-guide pre { background: #f8fafc; padding: 10px; border-radius: 6px; font-size: 12px; color: #64748b; overflow-x: auto; margin: 0 0 10px; } .slider-captcha-demo-tips { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } .scc-tip-card { background: white; border-radius: 14px; padding: 20px; border: 1px solid #e8ecf1; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); display: flex; gap: 14px; transition: all 0.3s ease; } .scc-tip-card:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); } .scc-tip-icon { font-size: 28px; flex-shrink: 0; line-height: 1; } .scc-tip-content h4 { font-size: 14px; font-weight: 700; color: #1e293b; margin: 0 0 6px; } .scc-tip-content p { font-size: 13px; color: #64748b; margin: 0; line-height: 1.6; } @keyframes sccFadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } @media (max-width: 768px) { .slider-captcha-demo-hero { min-height: 160px; padding: 30px 16px; } .slider-captcha-demo-hero-title { font-size: 24px; } .slider-captcha-demo-container { padding: 0 12px 40px; margin-top: -20px; } .scc-demo-main { grid-template-columns: 1fr; } .slider-captcha-demo-tips { grid-template-columns: 1fr; gap: 10px; } } ``` --- ## Task 5: 在 Utility.tsx 中注册滑块验证码工具 **Files:** - Modify: `src/pages/Utility/Utility.tsx` - [ ] **Step 7: 导入 SafetyOutlined 图标** 在 `Utility.tsx` 的 import 区域添加: ```tsx import { ToolOutlined, SearchOutlined, CalculatorOutlined, CalendarOutlined, QrcodeOutlined, LinkOutlined, ClockCircleOutlined, TranslationOutlined, CompressOutlined, FileTextOutlined, CodeOutlined, PictureOutlined, ThunderboltOutlined, EditOutlined, SafetyOutlined, // 新增 } from "@ant-design/icons"; ``` - [ ] **Step 8: 在 utilityTools 数组中添加滑块验证码条目** 在 `utilityTools` 数组末尾添加: ```tsx { id: 16, name: "滑块验证码", desc: "拼图滑块验证码组件,支持拖拽验证,可集成到登录注册流程", icon: , color: "#10b981", category: "实用", usageCount: 2890, }, ``` - [ ] **Step 9: 在 routeMap 中添加滑块验证码路由映射** 在 `handleToolClick` 的 `routeMap` 中添加: ```tsx "滑块验证码": "/utility/slider-captcha", ``` --- ## Task 6: 在 App.tsx 中添加路由配置 **Files:** - Modify: `src/App.tsx` - [ ] **Step 10: 添加 lazy import** 在 `App.tsx` 的 import 区域添加: ```tsx const SliderCaptchaDemo = lazy(() => import("@/pages/SliderCaptchaDemo/SliderCaptchaDemo")); ``` - [ ] **Step 11: 在 Routes 中添加新路由** 在 `` 中添加: ```tsx } /> ``` --- ## Self-Review ### 1. Spec coverage | 需求 | 对应 Task | |------|----------| | Canvas 生成拼图验证码 | Task 1 | | 滑块拖拽交互(鼠标+触摸) | Task 2 | | 验证逻辑(容错计算) | Task 1 + Task 2 | | 成功/失败状态反馈 | Task 2 | | 刷新重置功能 | Task 2 | | 组件导出入口 | Task 3 | | 演示页面(参数调节+代码示例) | Task 4 | | 工具列表注册 | Task 5 | | 路由配置 | Task 6 | **无遗漏。** ### 2. Placeholder scan - 无 "TBD"、"TODO"、"implement later" - 所有步骤包含完整代码 - 无 "Similar to Task N" 引用 - 所有文件路径精确 ### 3. Type consistency - `CaptchaResult` 和 `GapPosition` 接口在 Task 1 中定义,Task 2 中一致使用 - `SliderCaptchaProps` 接口在 Task 2 中定义并导出 - `CaptchaStatus` 类型在 Task 2 中定义并一致使用 - `useRecordHistory` 参数格式与现有代码一致 --- ## 执行选项 **计划已保存到 `docs/plans/2026-06-13-slider-captcha-api.md`。两个执行选项:** **1. Subagent-Driven(推荐)** - 每个 Task 分配一个独立子代理执行,逐个审查 **2. Inline Execution** - 在当前会话中按顺序批量执行 **请选择执行方式?**