|
@@ -1,12 +1,12 @@
|
|
|
"use client"
|
|
"use client"
|
|
|
|
|
|
|
|
-import React, { useState, useEffect } from "react"
|
|
|
|
|
|
|
+import React, { useState } from "react"
|
|
|
import Image from "next/image"
|
|
import Image from "next/image"
|
|
|
import { Button, Form, Input, message, Modal, Pagination, ConfigProvider } from "antd"
|
|
import { Button, Form, Input, message, Modal, Pagination, ConfigProvider } from "antd"
|
|
|
import Link from "next/link";
|
|
import Link from "next/link";
|
|
|
-import { clientPost } from "@/utils/request";
|
|
|
|
|
-
|
|
|
|
|
-// 留言表单数据接口(移除quantity字段)
|
|
|
|
|
|
|
+import { clientPost } from "@/utils/clientRequest";
|
|
|
|
|
+import type { ApiResponse } from '@/utils/clientRequest'
|
|
|
|
|
+// 留言表单数据接口
|
|
|
interface QuoteFormValues {
|
|
interface QuoteFormValues {
|
|
|
name: string;
|
|
name: string;
|
|
|
phone: string;
|
|
phone: string;
|
|
@@ -22,7 +22,7 @@ interface SaveQuotePayload {
|
|
|
phone: string;
|
|
phone: string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 补充 ProductItem 接口定义(避免类型报错)
|
|
|
|
|
|
|
+// ProductItem 接口定义
|
|
|
interface ProductItem {
|
|
interface ProductItem {
|
|
|
productId: string;
|
|
productId: string;
|
|
|
productName: string;
|
|
productName: string;
|
|
@@ -36,7 +36,8 @@ interface ProductGridProps {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export default function ProductGrid({ products }: ProductGridProps) {
|
|
export default function ProductGrid({ products }: ProductGridProps) {
|
|
|
- const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL as string
|
|
|
|
|
|
|
+ // 修复:增加空值判断,避免环境变量未定义报错
|
|
|
|
|
+ const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || ''
|
|
|
|
|
|
|
|
// 分页控制
|
|
// 分页控制
|
|
|
const [currentPage, setCurrentPage] = useState(1)
|
|
const [currentPage, setCurrentPage] = useState(1)
|
|
@@ -46,29 +47,27 @@ export default function ProductGrid({ products }: ProductGridProps) {
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
|
|
const [currentProduct, setCurrentProduct] = useState<ProductItem | null>(null)
|
|
const [currentProduct, setCurrentProduct] = useState<ProductItem | null>(null)
|
|
|
const [form] = Form.useForm()
|
|
const [form] = Form.useForm()
|
|
|
- // 添加加载状态,防止重复提交
|
|
|
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
|
|
|
|
|
|
|
|
+ // 创建 message 实例(AntD v5 必须)
|
|
|
const [messageApi, contextHolder] = message.useMessage()
|
|
const [messageApi, contextHolder] = message.useMessage()
|
|
|
|
|
|
|
|
- // 当前页数据
|
|
|
|
|
- const startIndex = (currentPage - 1) * pageSize
|
|
|
|
|
|
|
+ // 当前页数据(增加边界保护)
|
|
|
|
|
+ const startIndex = Math.max(0, (currentPage - 1) * pageSize)
|
|
|
const currentProducts = products.slice(startIndex, startIndex + pageSize)
|
|
const currentProducts = products.slice(startIndex, startIndex + pageSize)
|
|
|
|
|
|
|
|
- // 打开弹窗并记录当前产品(移除quantity初始化)
|
|
|
|
|
|
|
+ // 打开弹窗
|
|
|
const showModal = (product: ProductItem) => {
|
|
const showModal = (product: ProductItem) => {
|
|
|
setCurrentProduct(product)
|
|
setCurrentProduct(product)
|
|
|
setIsModalOpen(true)
|
|
setIsModalOpen(true)
|
|
|
- // 延迟设置表单值,确保Modal渲染完成后再初始化Form
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- form.setFieldsValue({
|
|
|
|
|
- productId: product.productId,
|
|
|
|
|
- productName: product.productName
|
|
|
|
|
- })
|
|
|
|
|
- }, 0)
|
|
|
|
|
|
|
+ // 立即设置表单值(无需 setTimeout)
|
|
|
|
|
+ form.setFieldsValue({
|
|
|
|
|
+ productId: product.productId,
|
|
|
|
|
+ productName: product.productName
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 关闭弹窗(优化:增加延迟,确保提示显示完成)
|
|
|
|
|
|
|
+ // 关闭弹窗
|
|
|
const handleCancel = () => {
|
|
const handleCancel = () => {
|
|
|
setIsModalOpen(false)
|
|
setIsModalOpen(false)
|
|
|
setCurrentProduct(null)
|
|
setCurrentProduct(null)
|
|
@@ -76,101 +75,129 @@ export default function ProductGrid({ products }: ProductGridProps) {
|
|
|
setSubmitting(false)
|
|
setSubmitting(false)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 提交报价表单(完善接口调用逻辑)
|
|
|
|
|
|
|
+ // 提交报价表单(核心修复)
|
|
|
const handleSubmit = async (values: QuoteFormValues) => {
|
|
const handleSubmit = async (values: QuoteFormValues) => {
|
|
|
try {
|
|
try {
|
|
|
- // 1. 设置提交中状态,禁用按钮
|
|
|
|
|
setSubmitting(true)
|
|
setSubmitting(true)
|
|
|
|
|
|
|
|
- const response = await clientPost('/webSite/save', {
|
|
|
|
|
- content: values.message,
|
|
|
|
|
- deviceName: values.productName,
|
|
|
|
|
|
|
+ // 直接使用 clientPost 发送 POST 请求
|
|
|
|
|
+ const response: ApiResponse = await clientPost('/webSite/save', {
|
|
|
name: values.name,
|
|
name: values.name,
|
|
|
- phone: values.phone
|
|
|
|
|
|
|
+ phone: values.phone,
|
|
|
|
|
+ content: values.message,
|
|
|
|
|
+ deviceName: values.productName
|
|
|
}, {
|
|
}, {
|
|
|
- timeout: 15000,
|
|
|
|
|
|
|
+ timeout: 20000, // 自定义超时
|
|
|
headers: {
|
|
headers: {
|
|
|
- 'X-Client-Type': 'browser'
|
|
|
|
|
|
|
+ 'X-Custom-Header': 'custom-value'
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- // 关键修复2:使用独立的 messageApi 显示提示
|
|
|
|
|
|
|
+ // 显示成功提示并延迟关闭弹窗
|
|
|
messageApi.success("报价请求提交成功!我们会尽快联系你")
|
|
messageApi.success("报价请求提交成功!我们会尽快联系你")
|
|
|
handleCancel()
|
|
handleCancel()
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- // 异常处理也使用 messageApi
|
|
|
|
|
- const errorMsg = error instanceof Error ? error.message : "提交失败,请稍后重试"
|
|
|
|
|
|
|
+ // 更友好的错误提示
|
|
|
|
|
+ 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)
|
|
messageApi.error(errorMsg)
|
|
|
|
|
+ console.error("提交报价失败:", error)
|
|
|
} finally {
|
|
} finally {
|
|
|
- // 只有失败时才在这里重置状态(成功时在提示关闭后重置)
|
|
|
|
|
- if (submitting) {
|
|
|
|
|
- setSubmitting(false)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 无论成功失败都重置提交状态
|
|
|
|
|
+ setSubmitting(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理图片路径(增加容错)
|
|
|
|
|
+ const getProductImageUrl = (product: ProductItem) => {
|
|
|
|
|
+ if (product.productUrl) {
|
|
|
|
|
+ return `${BASE_URL}${product.productUrl}`
|
|
|
}
|
|
}
|
|
|
|
|
+ return "/assets/productions/2.png"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- // 关键修复3:添加 ConfigProvider 包裹组件,确保 AntD 上下文正确
|
|
|
|
|
<ConfigProvider>
|
|
<ConfigProvider>
|
|
|
- {/* 必须添加 contextHolder 到组件中,否则 message 无法显示 */}
|
|
|
|
|
- {contextHolder}
|
|
|
|
|
|
|
+ {contextHolder} {/* 必须挂载 message 上下文 */}
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-6">
|
|
<div className="flex flex-col gap-6">
|
|
|
- {/* 产品网格 */}
|
|
|
|
|
- <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">
|
|
|
|
|
- <Image
|
|
|
|
|
- className={"h-50 object-contain"}
|
|
|
|
|
- src={product.productUrl ? BASE_URL + product.productUrl : "/assets/productions/2.png"}
|
|
|
|
|
- alt={product.productName}
|
|
|
|
|
- width={1000}
|
|
|
|
|
- height={1000}
|
|
|
|
|
- priority={false}
|
|
|
|
|
- />
|
|
|
|
|
- </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">
|
|
|
|
|
- <button
|
|
|
|
|
- onClick={() => showModal(product)}
|
|
|
|
|
- className="bg-red-600 text-white !text-white px-4 !px-4 py-1.5 !py-1.5 rounded hover:bg-red-700 transition-colors duration-200"
|
|
|
|
|
- >
|
|
|
|
|
- 获取报价
|
|
|
|
|
- </button>
|
|
|
|
|
- <Link
|
|
|
|
|
- href={`/products/${product.productId}`}
|
|
|
|
|
- className="bg-blue-600 text-white !text-white px-4 !px-4 py-1.5 !py-1.5 rounded hover:bg-blue-700 transition-colors duration-200 ml-2"
|
|
|
|
|
- >
|
|
|
|
|
- 了解详情
|
|
|
|
|
- </Link>
|
|
|
|
|
|
|
+ {/* 产品网格(增加空数据处理) */}
|
|
|
|
|
+ {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>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 分页器 */}
|
|
|
|
|
- <div className="flex justify-center">
|
|
|
|
|
- <Pagination
|
|
|
|
|
- current={currentPage}
|
|
|
|
|
- pageSize={pageSize}
|
|
|
|
|
- total={products.length}
|
|
|
|
|
- onChange={(page) => setCurrentPage(page)}
|
|
|
|
|
- showSizeChanger={false}
|
|
|
|
|
- />
|
|
|
|
|
- </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
|
|
<Modal
|
|
|
- title={`获取 "${currentProduct?.productName}" 报价`}
|
|
|
|
|
|
|
+ title={`获取 "${currentProduct?.productName || '产品'}" 报价`}
|
|
|
open={isModalOpen}
|
|
open={isModalOpen}
|
|
|
onCancel={handleCancel}
|
|
onCancel={handleCancel}
|
|
|
footer={null}
|
|
footer={null}
|
|
@@ -181,6 +208,11 @@ export default function ProductGrid({ products }: ProductGridProps) {
|
|
|
form={form}
|
|
form={form}
|
|
|
layout="vertical"
|
|
layout="vertical"
|
|
|
onFinish={handleSubmit}
|
|
onFinish={handleSubmit}
|
|
|
|
|
+ initialValues={{
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ phone: '',
|
|
|
|
|
+ message: ''
|
|
|
|
|
+ }}
|
|
|
>
|
|
>
|
|
|
{/* 隐藏字段 */}
|
|
{/* 隐藏字段 */}
|
|
|
<Form.Item name="productId" hidden>
|
|
<Form.Item name="productId" hidden>
|
|
@@ -194,21 +226,32 @@ export default function ProductGrid({ products }: ProductGridProps) {
|
|
|
<Form.Item
|
|
<Form.Item
|
|
|
name="name"
|
|
name="name"
|
|
|
label="联系人姓名"
|
|
label="联系人姓名"
|
|
|
- rules={[{ required: true, message: "请输入您的姓名" }]}
|
|
|
|
|
|
|
+ rules={[{
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ message: "请输入您的姓名",
|
|
|
|
|
+ whitespace: true // 禁止纯空格
|
|
|
|
|
+ }]}
|
|
|
>
|
|
>
|
|
|
- <Input placeholder="请输入您的姓名" />
|
|
|
|
|
|
|
+ <Input placeholder="请输入您的姓名" disabled={submitting} />
|
|
|
</Form.Item>
|
|
</Form.Item>
|
|
|
|
|
|
|
|
- {/* 联系电话 */}
|
|
|
|
|
|
|
+ {/* 联系电话(优化正则) */}
|
|
|
<Form.Item
|
|
<Form.Item
|
|
|
name="phone"
|
|
name="phone"
|
|
|
label="联系电话"
|
|
label="联系电话"
|
|
|
rules={[
|
|
rules={[
|
|
|
{ required: true, message: "请输入您的联系电话" },
|
|
{ required: true, message: "请输入您的联系电话" },
|
|
|
- { pattern: /^1[3-9]\d{9}$/, message: "请输入有效的手机号码" }
|
|
|
|
|
|
|
+ {
|
|
|
|
|
+ pattern: /^1[3-9]\d{9}$/,
|
|
|
|
|
+ message: "请输入有效的11位手机号码"
|
|
|
|
|
+ }
|
|
|
]}
|
|
]}
|
|
|
>
|
|
>
|
|
|
- <Input placeholder="请输入您的手机号码" />
|
|
|
|
|
|
|
+ <Input
|
|
|
|
|
+ placeholder="请输入您的手机号码"
|
|
|
|
|
+ maxLength={11}
|
|
|
|
|
+ disabled={submitting}
|
|
|
|
|
+ />
|
|
|
</Form.Item>
|
|
</Form.Item>
|
|
|
|
|
|
|
|
{/* 留言/备注 */}
|
|
{/* 留言/备注 */}
|
|
@@ -219,13 +262,18 @@ export default function ProductGrid({ products }: ProductGridProps) {
|
|
|
<Input.TextArea
|
|
<Input.TextArea
|
|
|
rows={4}
|
|
rows={4}
|
|
|
placeholder="请输入您的其他需求或备注信息(选填)"
|
|
placeholder="请输入您的其他需求或备注信息(选填)"
|
|
|
|
|
+ disabled={submitting}
|
|
|
/>
|
|
/>
|
|
|
</Form.Item>
|
|
</Form.Item>
|
|
|
|
|
|
|
|
- {/* 表单操作按钮(添加加载状态) */}
|
|
|
|
|
|
|
+ {/* 表单操作按钮 */}
|
|
|
<Form.Item>
|
|
<Form.Item>
|
|
|
<div className="flex justify-end gap-2">
|
|
<div className="flex justify-end gap-2">
|
|
|
- <Button onClick={handleCancel} type="default" disabled={submitting}>
|
|
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={handleCancel}
|
|
|
|
|
+ type="default"
|
|
|
|
|
+ disabled={submitting}
|
|
|
|
|
+ >
|
|
|
取消
|
|
取消
|
|
|
</Button>
|
|
</Button>
|
|
|
<Button
|
|
<Button
|