Pārlūkot izejas kodu

跟新留言报价的验证功能

丁烨烨 4 mēneši atpakaļ
vecāks
revīzija
76b7e001e4
1 mainītis faili ar 173 papildinājumiem un 8 dzēšanām
  1. 173 8
      src/components/products/ProductGridClient.tsx

+ 173 - 8
src/components/products/ProductGridClient.tsx

@@ -1,18 +1,20 @@
 "use client"
 
-import React, { useState } from "react"
+import React, { useState, useRef, useEffect } from "react"
 import Image from "next/image"
 import { Button, Form, Input, message, Modal, Pagination, ConfigProvider } from "antd"
 import Link from "next/link";
 import { clientPost } from "@/utils/clientRequest";
 import type { ApiResponse } from '@/utils/clientRequest'
-// 留言表单数据接口
+
+// 留言表单数据接口(新增 verifyCode 字段)
 interface QuoteFormValues {
     name: string;
     phone: string;
     message: string;
     productId: string;
     productName: string;
+    verifyCode: string; // 新增:验证码字段
 }
 
 interface SaveQuotePayload {
@@ -49,6 +51,19 @@ export default function ProductGrid({ products }: ProductGridProps) {
     const [form] = Form.useForm()
     const [submitting, setSubmitting] = useState(false)
 
+    // 新增:验证码相关状态与Ref(无第三方依赖)
+    const [correctVerifyCode, setCorrectVerifyCode] = useState<string>('') // 存储正确的验证码
+    const canvasRef = useRef<HTMLCanvasElement>(null) // canvas 元素Ref
+    // 验证码配置(可自定义)
+    const verifyCodeConfig = {
+        length: 4, // 验证码长度
+        width: 120, // 验证码图片宽度
+        height: 40, // 验证码图片高度
+        fontSize: 20, // 字符字体大小
+        lineCount: 4, // 干扰线数量
+        dotCount: 50, // 干扰点数量
+    }
+
     // 创建 message 实例(AntD v5 必须)
     const [messageApi, contextHolder] = message.useMessage()
 
@@ -56,15 +71,117 @@ export default function ProductGrid({ products }: ProductGridProps) {
     const startIndex = Math.max(0, (currentPage - 1) * pageSize)
     const currentProducts = products.slice(startIndex, startIndex + pageSize)
 
-    // 打开弹窗
+    // 核心:生成随机验证码字符串(数字+大小写字母)
+    const generateVerifyCode = (length: number): string => {
+        const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+        let code = ''
+        for (let i = 0; i < length; i++) {
+            const randomIndex = Math.floor(Math.random() * chars.length)
+            code += chars[randomIndex]
+        }
+        return code
+    }
+
+    // 核心:绘制验证码图片(利用 canvas)
+    const drawVerifyCode = () => {
+        const canvas = canvasRef.current
+        if (!canvas) return
+
+        const ctx = canvas.getContext('2d')
+        if (!ctx) return
+
+        // 1. 重置 canvas(清空原有内容)
+        ctx.clearRect(0, 0, verifyCodeConfig.width, verifyCodeConfig.height)
+
+        // 2. 绘制背景(浅灰色,带圆角视觉效果)
+        ctx.fillStyle = '#f5f5f5'
+        ctx.fillRect(0, 0, verifyCodeConfig.width, verifyCodeConfig.height)
+        ctx.strokeStyle = '#d9d9d9'
+        ctx.strokeRect(0, 0, verifyCodeConfig.width, verifyCodeConfig.height)
+
+        // 3. 生成并存储正确验证码
+        const code = generateVerifyCode(verifyCodeConfig.length)
+        setCorrectVerifyCode(code)
+
+        // 4. 绘制验证码字符(随机位置、旋转角度,增加识别难度)
+        ctx.font = `${verifyCodeConfig.fontSize}px Arial`
+        ctx.textBaseline = 'middle'
+
+        const charWidth = verifyCodeConfig.width / code.length
+        for (let i = 0; i < code.length; i++) {
+            const char = code[i]
+            // 随机字符颜色(深色调,避免与背景混淆)
+            ctx.fillStyle = `rgb(${Math.floor(Math.random() * 80)}, ${Math.floor(Math.random() * 80)}, ${Math.floor(Math.random() * 80)})`
+            // 随机旋转角度(-30° 到 30°)
+            const rotateAngle = (Math.random() * 60 - 30) * Math.PI / 180
+            // 保存当前画布状态
+            ctx.save()
+            // 移动画布原点到字符绘制位置
+            ctx.translate(
+                i * charWidth + charWidth / 2,
+                verifyCodeConfig.height / 2
+            )
+            // 旋转画布
+            ctx.rotate(rotateAngle)
+            // 绘制字符
+            ctx.fillText(
+                char,
+                -verifyCodeConfig.fontSize / 2,
+                0
+            )
+            // 恢复画布状态
+            ctx.restore()
+        }
+
+        // 5. 绘制干扰线(随机位置、颜色、粗细)
+        for (let i = 0; i < verifyCodeConfig.lineCount; i++) {
+            ctx.strokeStyle = `rgb(${Math.floor(Math.random() * 150)}, ${Math.floor(Math.random() * 150)}, ${Math.floor(Math.random() * 150)})`
+            ctx.lineWidth = Math.random() * 2 + 1
+            ctx.beginPath()
+            ctx.moveTo(
+                Math.random() * verifyCodeConfig.width,
+                Math.random() * verifyCodeConfig.height
+            )
+            ctx.lineTo(
+                Math.random() * verifyCodeConfig.width,
+                Math.random() * verifyCodeConfig.height
+            )
+            ctx.stroke()
+        }
+
+        // 6. 绘制干扰点(随机位置、颜色、大小)
+        for (let i = 0; i < verifyCodeConfig.dotCount; i++) {
+            ctx.fillStyle = `rgb(${Math.floor(Math.random() * 200)}, ${Math.floor(Math.random() * 200)}, ${Math.floor(Math.random() * 200)})`
+            ctx.beginPath()
+            ctx.arc(
+                Math.random() * verifyCodeConfig.width,
+                Math.random() * verifyCodeConfig.height,
+                Math.random() * 1.5,
+                0,
+                2 * Math.PI
+            )
+            ctx.fill()
+        }
+    }
+
+    // 新增:刷新验证码(供点击、弹窗打开时调用)
+    const refreshVerifyCode = () => {
+        if (submitting) return // 提交中不允许刷新
+        drawVerifyCode()
+    }
+
+    // 打开弹窗(新增:刷新验证码)
     const showModal = (product: ProductItem) => {
         setCurrentProduct(product)
         setIsModalOpen(true)
-        // 立即设置表单值(无需 setTimeout)
+        // 立即设置表单值
         form.setFieldsValue({
             productId: product.productId,
-            productName: product.productName
+            productName: product.productName,
+            verifyCode: '' // 初始化验证码输入框为空
         })
+        // 刷新验证码(避免重复使用同一个验证码)
+        setTimeout(refreshVerifyCode, 50) // 延迟少量时间,确保canvas已渲染
     }
 
     // 关闭弹窗
@@ -75,9 +192,19 @@ export default function ProductGrid({ products }: ProductGridProps) {
         setSubmitting(false)
     }
 
-    // 提交报价表单(核心修复
+    // 提交报价表单(新增:验证码校验
     const handleSubmit = async (values: QuoteFormValues) => {
         try {
+            // 新增:第一步先校验验证码(不区分大小写,提升用户体验)
+            if (values.verifyCode.toLowerCase() !== correctVerifyCode.toLowerCase()) {
+                messageApi.error("验证码输入错误,请重新输入")
+                // 验证码错误后刷新验证码,防止用户反复尝试
+                refreshVerifyCode()
+                // 清空验证码输入框
+                form.setFieldsValue({ verifyCode: '' })
+                return // 终止提交流程
+            }
+
             setSubmitting(true)
 
             // 直接使用 clientPost 发送 POST 请求
@@ -93,7 +220,7 @@ export default function ProductGrid({ products }: ProductGridProps) {
                 }
             })
 
-            // 显示成功提示并延迟关闭弹窗
+            // 显示成功提示并关闭弹窗
             messageApi.success("报价请求提交成功!我们会尽快联系你")
             handleCancel()
         } catch (error) {
@@ -124,6 +251,11 @@ export default function ProductGrid({ products }: ProductGridProps) {
         return "/assets/productions/2.png"
     }
 
+    // 初始化:组件挂载后绘制一次验证码(防止弹窗打开时验证码为空)
+    useEffect(() => {
+        refreshVerifyCode()
+    }, [])
+
     return (
         <ConfigProvider>
             {contextHolder} {/* 必须挂载 message 上下文 */}
@@ -211,7 +343,8 @@ export default function ProductGrid({ products }: ProductGridProps) {
                         initialValues={{
                             name: '',
                             phone: '',
-                            message: ''
+                            message: '',
+                            verifyCode: '' // 新增:初始化验证码输入框
                         }}
                     >
                         {/* 隐藏字段 */}
@@ -266,6 +399,38 @@ export default function ProductGrid({ products }: ProductGridProps) {
                             />
                         </Form.Item>
 
+                        {/* 新增:验证码输入项(无第三方依赖,基于 canvas) */}
+                        <Form.Item
+                            name="verifyCode"
+                            label="验证码"
+                            rules={[
+                                { required: true, message: "请输入图片中的验证码" },
+                                { whitespace: true, message: "验证码不能为空" }
+                            ]}
+                        >
+                            <div className="flex items-center gap-3">
+                                <Input
+                                    placeholder="请输入验证码(不区分大小写)"
+                                    maxLength={verifyCodeConfig.length}
+                                    disabled={submitting}
+                                    style={{ flex: 1 }}
+                                />
+                                {/* 原生 canvas 元素:绘制验证码图片 */}
+                                <canvas
+                                    ref={canvasRef}
+                                    width={verifyCodeConfig.width}
+                                    height={verifyCodeConfig.height}
+                                    onClick={refreshVerifyCode}
+                                    style={{
+                                        cursor: submitting ? 'not-allowed' : 'pointer',
+                                        borderRadius: 4,
+                                        userSelect: 'none'
+                                    }}
+                                    disabled={submitting}
+                                />
+                            </div>
+                        </Form.Item>
+
                         {/* 表单操作按钮 */}
                         <Form.Item>
                             <div className="flex justify-end gap-2">