浏览代码

refactor(ui): 重构UI组件实现Ant Design兼容性和交互体验优化

- 将CoreProducts组件中的按钮替换为div元素以改善语义化
- 实现AntdPatchProvider包装器解决React 19兼容性问题
- 重构NewsList组件提升可访问性和交互流畅度
- 优化产品网格组件支持验证码验证功能
- 提取验证码工具函数到独立模块增强代码复用
- 更新Next.js布局文件集成Ant Design补丁
- 调整TypeScript配置jsx选项为preserve模式
- 优化新闻列表卡片点击交互和样式表现
- 实现产品详情页面链接导航功能
- 优化表格单组件标签结构和悬停效果
nahida 4 月之前
父节点
当前提交
62c7a24b6d

+ 1 - 1
next-env.d.ts

@@ -1,6 +1,6 @@
 /// <reference types="next" />
 /// <reference types="next/image-types/global" />
-import "./.next/types/routes.d.ts";
+/// <reference path="./.next/types/routes.d.ts" />
 
 // NOTE: This file should not be edited
 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

+ 7 - 5
src/app/layout.tsx

@@ -1,7 +1,7 @@
 import type {Metadata} from "next";
 import "./globals.css";
 import {AntdRegistry} from "@ant-design/nextjs-registry";
-import '@ant-design/v5-patch-for-react-19';
+import AntdPatchProvider from "@/components/AntdPatchProvider";
 import HeaderBlock from "@/components/headerBlock";
 import FooterBlock from "@/components/footerBlock";
 import Script from "next/script";
@@ -23,10 +23,12 @@ export default function RootLayout({
         <html lang="en">
         <body>
         <AntdRegistry>
-            <HeaderBlock/>
-            {children}
-            {/*<ChatWidget/>*/}
-            <FooterBlock/>
+            <AntdPatchProvider>
+                <HeaderBlock/>
+                {children}
+                {/*<ChatWidget/>*/}
+                <FooterBlock/>
+            </AntdPatchProvider>
         </AntdRegistry>
         </body>
         <Script id="baidu-tongji">

+ 10 - 0
src/components/AntdPatchProvider.tsx

@@ -0,0 +1,10 @@
+'use client';
+
+import '@ant-design/v5-patch-for-react-19';
+import React from 'react';
+
+const AntdPatchProvider = ({ children }: { children: React.ReactNode }) => {
+  return <>{children}</>;
+};
+
+export default AntdPatchProvider;

+ 2 - 3
src/components/CoreProducts.tsx

@@ -65,9 +65,8 @@ export default function CoreProducts({products}: ProductMenuProps) {
       <div className="w-full border border-gray-300 rounded-md overflow-hidden">
         <div className="flex overflow-x-auto">
           {tabs.map((tab) => (
-            <button
+            <div
               key={tab}
-              type="button"
               onClick={() => setActiveTab(tab)}
               className={`flex-1 min-w-36 sm:min-w-44 py-3 sm:py-4 text-center shuheiti transition-colors cursor-pointer ${
                 activeTab === tab
@@ -76,7 +75,7 @@ export default function CoreProducts({products}: ProductMenuProps) {
               }`}
             >
               {tab}
-            </button>
+            </div>
           ))}
         </div>
       </div>

+ 37 - 33
src/components/news/NewsList.tsx

@@ -42,45 +42,47 @@ const NewsItem: React.FC<NewsItemProps> = ({news: article}) => {
   const coverImageUrl = article.newsUrl ? `${BASE_URL}${article.newsUrl}` : '';
 
   return (
-      <div
-          className="group flex flex-col md:flex-row md:items-start bg-white rounded-xl p-[15px] md:p-5 shadow-[0_4px_15px_rgba(0,0,0,0.05)] hover:shadow-[0_8px_25px_rgba(0,0,0,0.1)] transition-[transform,box-shadow] duration-300 ease-in-out gap-[15px] md:gap-5 hover:-translate-y-[3px]"
+      <Link
+          href={`/news/${article.id}`}
+          className="group flex flex-col md:flex-row md:items-start bg-white rounded-xl p-5 shadow-md hover:shadow-xl transition-all duration-300 ease-in-out gap-5 hover:-translate-y-1 block"
+          aria-label={`查看${article.newsName}详情`}
       >
-        <div className="shrink-0 w-full h-[180px] md:w-[150px] md:h-[100px] rounded-lg overflow-hidden">
+        <div className="shrink-0 w-full h-44 md:w-44 md:h-28 rounded-lg overflow-hidden bg-gray-100 relative">
           <Image
               width={1000}
               height={1000}
               src={coverImageUrl}
               alt={article.newsName} // 补充有意义的alt属性,优化可访问性
               loading="lazy"
-              className="w-full h-full object-cover transition-transform duration-300 ease-in-out group-hover:scale-[1.05]"
+              className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
               onError={(e) => {
                 // 图片加载失败兜底,提升用户体验
                 (e.target as HTMLImageElement).src = '/default-news-cover.png';
               }}
           />
         </div>
-        <div className="flex-1 overflow-hidden">
-          <h3
-              className="text-[1.1rem] md:text-[1.2rem] text-gray-800 font-semibold mb-[10px] leading-snug line-clamp-2 md:line-clamp-1">
-            {article.newsName}
-          </h3>
-          <div className="flex items-center justify-between mb-2 text-[0.85rem] text-gray-400">
-            <span className="md:mx-5">{article.newsDesc}</span>
-            {/* 可选字段兜底,避免空值展示 */}
-            <span className="whitespace-nowrap">作者:{article.newsAuthor || '未知'}</span>
+        <div className="flex-1 overflow-hidden flex flex-col justify-between py-1">
+          <div>
+            <h3
+                className="text-lg md:text-xl text-gray-800 font-bold mb-2 leading-snug line-clamp-2 group-hover:text-blue-600 transition-colors">
+              {article.newsName}
+            </h3>
+            <div className="flex items-center gap-4 mb-2 text-sm text-gray-400">
+              <span>{article.newsDesc}</span>
+              {/* 可选字段兜底,避免空值展示 */}
+              {article.newsAuthor && <span className="whitespace-nowrap">作者:{article.newsAuthor}</span>}
+            </div>
+            <p className="text-sm md:text-base text-gray-600 leading-relaxed line-clamp-2">
+              {article.content}
+            </p>
           </div>
-          <p className="text-[0.9rem] md:text-[0.95rem] text-gray-600 leading-relaxed line-clamp-3 md:line-clamp-2">
-            {article.content}
-          </p>
         </div>
-        <Link
-            href={`/news/${article.id}`}
-            className="text-blue-500 hover:underline mt-4 self-start text-sm sm:text-base"
-            aria-label={`查看${article.newsName}详情`} // 优化可访问性
+        <div
+            className="mt-2 md:mt-0 md:self-center text-blue-500 font-medium text-sm sm:text-base shrink-0 flex items-center group-hover:text-blue-600"
         >
-          了解更多 &gt;
-        </Link>
-      </div>
+          了解更多 <span className="ml-1 transition-transform group-hover:translate-x-1">&gt;</span>
+        </div>
+      </Link>
   );
 };
 
@@ -88,7 +90,7 @@ const NewsItem: React.FC<NewsItemProps> = ({news: article}) => {
 const NewsList: React.FC<NewsListProps> = ({newsList}) => {
   // 空数组判断优化,避免length判断出错
   if (!Array.isArray(newsList) || newsList.length === 0) {
-    return <div className="text-center text-[1.2rem] text-gray-400 py-10">暂无相关新闻</div>;
+    return <div className="text-center text-lg text-gray-400 py-10">暂无相关新闻</div>;
   }
 
   return (
@@ -138,14 +140,15 @@ const NewsPage: React.FC<NewsPageProps> = ({newsList = []}) => {
   };
 
   return (
-      <div className="max-w-[1200px] mx-auto min-h-screen px-4">
-        <div className="flex justify-center mb-[30px] gap-[10px]">
+      <div className="max-w-6xl mx-auto min-h-screen px-4">
+        <div className="flex justify-center mb-8 gap-4">
           {/* 核心修复:提升文字样式优先级 + 调整类名顺序 */}
           <button
-              className={`px-[30px] py-3 rounded-full transition-all duration-300 ease-in-out outline-none font-medium
+              disabled={activeTab === 'company'}
+              className={`px-8 py-3 rounded-full transition-all duration-300 ease-in-out outline-none font-medium
             ${activeTab === 'company'
-                  ? 'bg-[#2f54eb] text-white !text-white shadow-[0_4px_12px_rgba(47,84,235,0.3)]'
-                  : 'bg-gray-200 text-gray-500 hover:bg-gray-300 hover:text-gray-800'
+                  ? 'bg-blue-600 text-white shadow-md cursor-default'
+                  : 'bg-gray-200 text-gray-500 hover:bg-gray-300 hover:text-gray-800 cursor-pointer'
               }`}
               onClick={() => handleTabChange('company')}
               aria-pressed={activeTab === 'company'}
@@ -154,10 +157,11 @@ const NewsPage: React.FC<NewsPageProps> = ({newsList = []}) => {
             公司新闻
           </button>
           <button
-              className={`px-[30px] py-3 rounded-full transition-all duration-300 ease-in-out outline-none font-medium
+              disabled={activeTab === 'industry'}
+              className={`px-8 py-3 rounded-full transition-all duration-300 ease-in-out outline-none font-medium
             ${activeTab === 'industry'
-                  ? 'bg-[#2f54eb] text-white !text-white shadow-[0_4px_12px_rgba(47,84,235,0.3)]'
-                  : 'bg-gray-200 text-gray-500 hover:bg-gray-300 hover:text-gray-800'
+                  ? 'bg-blue-600 text-white !text-white shadow-md cursor-default'
+                  : 'bg-gray-200 text-gray-500 hover:bg-gray-300 hover:text-gray-800 cursor-pointer'
               }`}
               onClick={() => handleTabChange('industry')}
               aria-pressed={activeTab === 'industry'}
@@ -167,7 +171,7 @@ const NewsPage: React.FC<NewsPageProps> = ({newsList = []}) => {
           </button>
         </div>
 
-        {isLoading && <div className="text-center text-[1.2rem] text-gray-500 py-10">加载中...</div>}
+        {isLoading && <div className="text-center text-lg text-gray-500 py-10">加载中...</div>}
         {!isLoading && <NewsList newsList={filteredNews}/>}
       </div>
   );

+ 348 - 432
src/components/products/ProductGridClient.tsx

@@ -1,459 +1,375 @@
 "use client"
 
-import React, { useState, useRef, useEffect } from "react"
+import React, {useEffect, useRef, useState} from "react"
 import Image from "next/image"
-import { Button, Form, Input, message, Modal, Pagination, ConfigProvider } from "antd"
+import {Button, ConfigProvider, Form, Input, message, Modal, Pagination} from "antd"
 import Link from "next/link";
-import { clientPost } from "@/utils/clientRequest";
-import type { ApiResponse } from '@/utils/clientRequest'
+import type {ApiResponse} from '@/utils/clientRequest'
+import {clientPost} from "@/utils/clientRequest";
+import {defaultVerifyCodeConfig, drawVerifyCode as drawVerifyCodeUtil} from "@/utils/verifyCodeUtils";
 
 // 留言表单数据接口(新增 verifyCode 字段)
 interface QuoteFormValues {
-    name: string;
-    phone: string;
-    message: string;
-    productId: string;
-    productName: string;
-    verifyCode: string; // 新增:验证码字段
+  name: string;
+  phone: string;
+  message: string;
+  productId: string;
+  productName: string;
+  verifyCode: string; // 新增:验证码字段
 }
 
 interface SaveQuotePayload {
-    content: string;
-    deviceName: string;
-    name: string;
-    phone: string;
+  content: string;
+  deviceName: string;
+  name: string;
+  phone: string;
 }
 
 // ProductItem 接口定义
 interface ProductItem {
-    productId: string;
-    productName: string;
-    productUrl?: string;
-    productIntroduction: string;
-    productModel: string;
+  productId: string;
+  productName: string;
+  productUrl?: string;
+  productIntroduction: string;
+  productModel: string;
 }
 
 interface ProductGridProps {
-    products: ProductItem[]
+  products: ProductItem[]
 }
 
-export default function ProductGrid({ products }: ProductGridProps) {
-    // 修复:增加空值判断,避免环境变量未定义报错
-    const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || ''
-
-    // 分页控制
-    const [currentPage, setCurrentPage] = useState(1)
-    const pageSize = 6
-
-    // 弹窗控制
-    const [isModalOpen, setIsModalOpen] = useState(false)
-    const [currentProduct, setCurrentProduct] = useState<ProductItem | null>(null)
-    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()
-
-    // 当前页数据(增加边界保护)
-    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
+export default function ProductGrid({products}: ProductGridProps) {
+  // 修复:增加空值判断,避免环境变量未定义报错
+  const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || ''
+
+  // 分页控制
+  const [currentPage, setCurrentPage] = useState(1)
+  const pageSize = 6
+
+  // 弹窗控制
+  const [isModalOpen, setIsModalOpen] = useState(false)
+  const [currentProduct, setCurrentProduct] = useState<ProductItem | null>(null)
+  const [form] = Form.useForm()
+  const [submitting, setSubmitting] = useState(false)
+
+  // 新增:验证码相关状态与Ref(无第三方依赖)
+  const [correctVerifyCode, setCorrectVerifyCode] = useState<string>('') // 存储正确的验证码
+  const canvasRef = useRef<HTMLCanvasElement>(null) // canvas 元素Ref
+  // 验证码配置(可自定义)
+  const verifyCodeConfig = defaultVerifyCodeConfig
+
+  // 创建 message 实例(AntD v5 必须)
+  const [messageApi, contextHolder] = message.useMessage()
+
+  // 当前页数据(增加边界保护)
+  const startIndex = Math.max(0, (currentPage - 1) * pageSize)
+  const currentProducts = products.slice(startIndex, startIndex + pageSize)
+
+  // 核心:绘制验证码图片(利用 canvas)
+  const drawVerifyCode = () => {
+    const canvas = canvasRef.current
+    if (!canvas) return
+    
+    const code = drawVerifyCodeUtil(canvas, verifyCodeConfig)
+    if (code) {
+      setCorrectVerifyCode(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)
+    // 立即设置表单值
+    form.setFieldsValue({
+      productId: product.productId,
+      productName: product.productName,
+      verifyCode: '' // 初始化验证码输入框为空
+    })
+    // 刷新验证码(避免重复使用同一个验证码)
+    setTimeout(refreshVerifyCode, 50) // 延迟少量时间,确保canvas已渲染
+  }
+
+  // 关闭弹窗
+  const handleCancel = () => {
+    setIsModalOpen(false)
+    setCurrentProduct(null)
+    form.resetFields()
+    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 请求
+      const response: ApiResponse = await clientPost('/webSite/save', {
+        name: values.name,
+        phone: values.phone,
+        content: values.message,
+        deviceName: values.productName
+      }, {
+        timeout: 20000, // 自定义超时
+        headers: {
+          'X-Custom-Header': 'custom-value'
         }
-    }
-
-    // 新增:刷新验证码(供点击、弹窗打开时调用)
-    const refreshVerifyCode = () => {
-        if (submitting) return // 提交中不允许刷新
-        drawVerifyCode()
-    }
-
-    // 打开弹窗(新增:刷新验证码)
-    const showModal = (product: ProductItem) => {
-        setCurrentProduct(product)
-        setIsModalOpen(true)
-        // 立即设置表单值
-        form.setFieldsValue({
-            productId: product.productId,
-            productName: product.productName,
-            verifyCode: '' // 初始化验证码输入框为空
-        })
-        // 刷新验证码(避免重复使用同一个验证码)
-        setTimeout(refreshVerifyCode, 50) // 延迟少量时间,确保canvas已渲染
-    }
-
-    // 关闭弹窗
-    const handleCancel = () => {
-        setIsModalOpen(false)
-        setCurrentProduct(null)
-        form.resetFields()
-        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 请求
-            const response: ApiResponse = await clientPost('/webSite/save', {
-                name: values.name,
-                phone: values.phone,
-                content: values.message,
-                deviceName: values.productName
-            }, {
-                timeout: 20000, // 自定义超时
-                headers: {
-                    'X-Custom-Header': 'custom-value'
-                }
-            })
-
-            // 显示成功提示并关闭弹窗
-            messageApi.success("报价请求提交成功!我们会尽快联系你")
-            handleCancel()
-        } catch (error) {
-            // 更友好的错误提示
-            let errorMsg = "提交失败,请稍后重试"
-            if (error instanceof Error) {
-                if (error.message.includes('Network')) {
-                    errorMsg = "网络连接失败,请检查您的网络"
-                } else if (error.message.includes('timeout')) {
-                    errorMsg = "请求超时,请稍后重试"
-                } else {
-                    errorMsg = error.message
-                }
-            }
-            messageApi.error(errorMsg)
-            console.error("提交报价失败:", error)
-        } finally {
-            // 无论成功失败都重置提交状态
-            setSubmitting(false)
+      })
+
+      // 显示成功提示并关闭弹窗
+      messageApi.success("报价请求提交成功!我们会尽快联系你")
+      handleCancel()
+    } catch (error) {
+      // 更友好的错误提示
+      let errorMsg = "提交失败,请稍后重试"
+      if (error instanceof Error) {
+        if (error.message.includes('Network')) {
+          errorMsg = "网络连接失败,请检查您的网络"
+        } else if (error.message.includes('timeout')) {
+          errorMsg = "请求超时,请稍后重试"
+        } else {
+          errorMsg = error.message
         }
+      }
+      messageApi.error(errorMsg)
+      console.error("提交报价失败:", error)
+    } finally {
+      // 无论成功失败都重置提交状态
+      setSubmitting(false)
     }
+  }
 
-    // 处理图片路径(增加容错)
-    const getProductImageUrl = (product: ProductItem) => {
-        if (product.productUrl) {
-            return `${BASE_URL}${product.productUrl}`
-        }
-        return "/assets/productions/2.png"
+  // 处理图片路径(增加容错)
+  const getProductImageUrl = (product: ProductItem) => {
+    if (product.productUrl) {
+      return `${BASE_URL}${product.productUrl}`
     }
-
-    // 初始化:组件挂载后绘制一次验证码(防止弹窗打开时验证码为空)
-    useEffect(() => {
-        refreshVerifyCode()
-    }, [])
-
-    return (
-        <ConfigProvider>
-            {contextHolder} {/* 必须挂载 message 上下文 */}
-
-            <div className="flex flex-col gap-6">
-                {/* 产品网格(增加空数据处理) */}
-                {currentProducts.length === 0 ? (
-                    <div className="text-center py-10">暂无产品数据</div>
-                ) : (
-                    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
-                        {currentProducts.map((product) => (
-                            <div
-                                key={product.productId}
-                                className="bg-white rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-[1.02]"
-                            >
-                                <div className="p-2 aspect-square relative">
-                                    <Image
-                                        className="h-full w-full object-contain"
-                                        src={getProductImageUrl(product)}
-                                        alt={product.productName || '产品图片'}
-                                        fill // Next.js 13+ 推荐用法
-                                        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
-                                        priority={false}
-                                        // 图片加载失败兜底
-                                        onError={(e) => {
-                                            e.currentTarget.src = "/assets/productions/2.png"
-                                        }}
-                                    />
-                                </div>
-                                <div className="p-4 sm:py-6 text-center">
-                                    <h3 className="font-bold text-lg mb-1">{product.productName}</h3>
-                                    <p className="text-sm text-gray-500 mb-2 line-clamp-1">
-                                        {product.productIntroduction || '暂无介绍'}
-                                    </p>
-                                    <p className="text-sm mb-3">{product.productModel || '暂无型号'}</p>
-
-                                    <div className="w-full flex justify-center gap-2">
-                                        <button
-                                            onClick={() => showModal(product)}
-                                            className="bg-red-600 text-white px-4 py-1.5 rounded hover:bg-red-700 transition-colors duration-200"
-                                            disabled={submitting}
-                                        >
-                                            获取报价
-                                        </button>
-                                        <Link
-                                            href={`/products/${product.productId}`}
-                                            className="bg-blue-600 text-white px-4 py-1.5 rounded hover:bg-blue-700 transition-colors duration-200"
-                                        >
-                                            了解详情
-                                        </Link>
-                                    </div>
-                                </div>
-                            </div>
-                        ))}
-                    </div>
-                )}
-
-                {/* 分页器(只有数据大于分页大小时显示) */}
-                {products.length > pageSize && (
-                    <div className="flex justify-center">
-                        <Pagination
-                            current={currentPage}
-                            pageSize={pageSize}
-                            total={products.length}
-                            onChange={(page) => setCurrentPage(page)}
-                            showSizeChanger={false}
-                            disabled={submitting}
-                        />
-                    </div>
-                )}
-
-                {/* 报价弹窗 */}
-                <Modal
-                    title={`获取 "${currentProduct?.productName || '产品'}" 报价`}
-                    open={isModalOpen}
-                    onCancel={handleCancel}
-                    footer={null}
-                    maskClosable={false}
-                    width={600}
+    return "/assets/productions/2.png"
+  }
+
+  // 初始化:组件挂载后绘制一次验证码(防止弹窗打开时验证码为空)
+  useEffect(() => {
+    refreshVerifyCode()
+  }, [])
+
+  return (
+    <ConfigProvider>
+      {contextHolder} {/* 必须挂载 message 上下文 */}
+
+      <div className="flex flex-col gap-6">
+        {/* 产品网格(增加空数据处理) */}
+        {currentProducts.length === 0 ? (
+          <div className="text-center py-10">暂无产品数据</div>
+        ) : (
+          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
+            {currentProducts.map((product) => (
+              <div
+                key={product.productId}
+                className="bg-white rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-[1.02]"
+              >
+                <Link
+                  href={`/products/${product.productId}`}
+                  className="block p-2 aspect-square relative cursor-pointer"
                 >
-                    <Form
-                        form={form}
-                        layout="vertical"
-                        onFinish={handleSubmit}
-                        initialValues={{
-                            name: '',
-                            phone: '',
-                            message: '',
-                            verifyCode: '' // 新增:初始化验证码输入框
-                        }}
+                  <Image
+                    className="h-full w-full object-contain"
+                    src={getProductImageUrl(product)}
+                    alt={product.productName || '产品图片'}
+                    fill // Next.js 13+ 推荐用法
+                    sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
+                    priority={false}
+                    // 图片加载失败兜底
+                    onError={(e) => {
+                      e.currentTarget.src = "/assets/productions/2.png"
+                    }}
+                  />
+                </Link>
+                <div className="p-4 sm:py-6 text-center">
+                  <h3 className="font-bold text-lg mb-1">{product.productName}</h3>
+                  <p className="text-sm text-gray-500 mb-2 line-clamp-1">
+                    {product.productIntroduction || '暂无介绍'}
+                  </p>
+                  <p className="text-sm mb-3">{product.productModel || '暂无型号'}</p>
+
+                  <div className="w-full flex justify-center gap-2">
+                    <div
+                      onClick={() => {
+                        if (!submitting) showModal(product)
+                      }}
+                      className={`flex items-center justify-center bg-red-600 text-white px-4 py-1.5 rounded hover:bg-red-700 transition-colors duration-200 text-sm leading-6 ${submitting ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
                     >
-                        {/* 隐藏字段 */}
-                        <Form.Item name="productId" hidden>
-                            <Input />
-                        </Form.Item>
-                        <Form.Item name="productName" hidden>
-                            <Input />
-                        </Form.Item>
-
-                        {/* 联系人姓名 */}
-                        <Form.Item
-                            name="name"
-                            label="联系人姓名"
-                            rules={[{
-                                required: true,
-                                message: "请输入您的姓名",
-                                whitespace: true // 禁止纯空格
-                            }]}
-                        >
-                            <Input placeholder="请输入您的姓名" disabled={submitting} />
-                        </Form.Item>
-
-                        {/* 联系电话(优化正则) */}
-                        <Form.Item
-                            name="phone"
-                            label="联系电话"
-                            rules={[
-                                { required: true, message: "请输入您的联系电话" },
-                                {
-                                    pattern: /^1[3-9]\d{9}$/,
-                                    message: "请输入有效的11位手机号码"
-                                }
-                            ]}
-                        >
-                            <Input
-                                placeholder="请输入您的手机号码"
-                                maxLength={11}
-                                disabled={submitting}
-                            />
-                        </Form.Item>
-
-                        {/* 留言/备注 */}
-                        <Form.Item
-                            name="message"
-                            label="留言/备注"
-                        >
-                            <Input.TextArea
-                                rows={4}
-                                placeholder="请输入您的其他需求或备注信息(选填)"
-                                disabled={submitting}
-                            />
-                        </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">
-                                <Button
-                                    onClick={handleCancel}
-                                    type="default"
-                                    disabled={submitting}
-                                >
-                                    取消
-                                </Button>
-                                <Button
-                                    type="primary"
-                                    htmlType="submit"
-                                    loading={submitting}
-                                    disabled={submitting}
-                                >
-                                    提交报价请求
-                                </Button>
-                            </div>
-                        </Form.Item>
-                    </Form>
-                </Modal>
-            </div>
-        </ConfigProvider>
-    )
-}
+                      获取报价
+                    </div>
+                    <Link
+                      href={`/products/${product.productId}`}
+                      className="flex items-center justify-center bg-blue-600 text-white px-4 py-1.5 rounded hover:bg-blue-700 transition-colors duration-200 text-sm leading-6"
+                    >
+                      了解详情
+                    </Link>
+                  </div>
+                </div>
+              </div>
+            ))}
+          </div>
+        )}
+
+        {/* 分页器(只有数据大于分页大小时显示) */}
+        {products.length > pageSize && (
+          <div className="flex justify-center">
+            <Pagination
+              current={currentPage}
+              pageSize={pageSize}
+              total={products.length}
+              onChange={(page) => setCurrentPage(page)}
+              showSizeChanger={false}
+              disabled={submitting}
+            />
+          </div>
+        )}
+
+        {/* 报价弹窗 */}
+        <Modal
+          title={`获取 "${currentProduct?.productName || '产品'}" 报价`}
+          open={isModalOpen}
+          onCancel={handleCancel}
+          footer={null}
+          maskClosable={false}
+          width={600}
+        >
+          <Form
+            form={form}
+            layout="vertical"
+            onFinish={handleSubmit}
+            initialValues={{
+              name: '',
+              phone: '',
+              message: '',
+              verifyCode: '' // 新增:初始化验证码输入框
+            }}
+          >
+            {/* 隐藏字段 */}
+            <Form.Item name="productId" hidden>
+              <Input/>
+            </Form.Item>
+            <Form.Item name="productName" hidden>
+              <Input/>
+            </Form.Item>
+
+            {/* 联系人姓名 */}
+            <Form.Item
+              name="name"
+              label="联系人姓名"
+              rules={[{
+                required: true,
+                message: "请输入您的姓名",
+                whitespace: true // 禁止纯空格
+              }]}
+            >
+              <Input placeholder="请输入您的姓名" disabled={submitting}/>
+            </Form.Item>
+
+            {/* 联系电话(优化正则) */}
+            <Form.Item
+              name="phone"
+              label="联系电话"
+              rules={[
+                {required: true, message: "请输入您的联系电话"},
+                {
+                  pattern: /^1[3-9]\d{9}$/,
+                  message: "请输入有效的11位手机号码"
+                }
+              ]}
+            >
+              <Input
+                placeholder="请输入您的手机号码"
+                maxLength={11}
+                disabled={submitting}
+              />
+            </Form.Item>
+
+            {/* 留言/备注 */}
+            <Form.Item
+              name="message"
+              label="留言/备注"
+            >
+              <Input.TextArea
+                rows={4}
+                placeholder="请输入您的其他需求或备注信息(选填)"
+                disabled={submitting}
+              />
+            </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">
+                <Button
+                  onClick={handleCancel}
+                  type="default"
+                  disabled={submitting}
+                >
+                  取消
+                </Button>
+                <Button
+                  type="primary"
+                  htmlType="submit"
+                  loading={submitting}
+                  disabled={submitting}
+                >
+                  提交报价请求
+                </Button>
+              </div>
+            </Form.Item>
+          </Form>
+        </Modal>
+      </div>
+    </ConfigProvider>
+  )
+}

+ 106 - 0
src/utils/verifyCodeUtils.ts

@@ -0,0 +1,106 @@
+export interface VerifyCodeConfig {
+  length: number;
+  width: number;
+  height: number;
+  fontSize: number;
+  lineCount: number;
+  dotCount: number;
+}
+
+export const defaultVerifyCodeConfig: VerifyCodeConfig = {
+  length: 4,
+  width: 120,
+  height: 40,
+  fontSize: 20,
+  lineCount: 4,
+  dotCount: 50,
+}
+
+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
+}
+
+export const drawVerifyCode = (canvas: HTMLCanvasElement, config: VerifyCodeConfig = defaultVerifyCodeConfig): string | null => {
+  const ctx = canvas.getContext('2d')
+  if (!ctx) return null
+
+  // 1. 重置 canvas(清空原有内容)
+  ctx.clearRect(0, 0, config.width, config.height)
+
+  // 2. 绘制背景(浅灰色,带圆角视觉效果)
+  ctx.fillStyle = '#f5f5f5'
+  ctx.fillRect(0, 0, config.width, config.height)
+  ctx.strokeStyle = '#d9d9d9'
+  ctx.strokeRect(0, 0, config.width, config.height)
+
+  // 3. 生成验证码
+  const code = generateVerifyCode(config.length)
+
+  // 4. 绘制验证码字符(随机位置、旋转角度,增加识别难度)
+  ctx.font = `${config.fontSize}px Arial`
+  ctx.textBaseline = 'middle'
+
+  const charWidth = config.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,
+      config.height / 2
+    )
+    // 旋转画布
+    ctx.rotate(rotateAngle)
+    // 绘制字符
+    ctx.fillText(
+      char,
+      -config.fontSize / 2,
+      0
+    )
+    // 恢复画布状态
+    ctx.restore()
+  }
+
+  // 5. 绘制干扰线(随机位置、颜色、粗细)
+  for (let i = 0; i < config.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() * config.width,
+      Math.random() * config.height
+    )
+    ctx.lineTo(
+      Math.random() * config.width,
+      Math.random() * config.height
+    )
+    ctx.stroke()
+  }
+
+  // 6. 绘制干扰点(随机位置、颜色、大小)
+  for (let i = 0; i < config.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() * config.width,
+      Math.random() * config.height,
+      Math.random() * 1.5,
+      0,
+      2 * Math.PI
+    )
+    ctx.fill()
+  }
+
+  return code
+}

+ 1 - 1
tsconfig.json

@@ -15,7 +15,7 @@
     "moduleResolution": "bundler",
     "resolveJsonModule": true,
     "isolatedModules": true,
-    "jsx": "react-jsx",
+    "jsx": "preserve",
     "incremental": true,
     "plugins": [
       {