|
|
@@ -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">
|