Prechádzať zdrojové kódy

feat(products):重构产品展示页面并添加动态解决方案详情页

- 将产品页面改为服务端渲染,使用serverGet获取产品分类数据- 移除客户端状态管理,使用ProductMenuClient组件处理交互
- 添加AnimatedSection动画效果和Suspense加载状态
- 新增解决方案详情页,支持动态路由和静态参数生成
- 集成富文本渲染和面包屑导航组件
- 实现缓存策略,设置180秒重新验证时间- 添加错误处理和内容未找到组件
nahida 7 mesiacov pred
rodič
commit
8cad9e2ef1
2 zmenil súbory, kde vykonal 102 pridanie a 270 odobranie
  1. 33 270
      src/app/products/page.tsx
  2. 69 0
      src/app/solutions/[id]/page.tsx

+ 33 - 270
src/app/products/page.tsx

@@ -1,286 +1,49 @@
-"use client"
-
-import { useState } from "react"
+import Image from "next/image"
 import MainTitle from "@/components/MainTitle"
-import SubTitle from "@/components/subTitle";
-import Image from "next/image";
-
-export default function ProductShowcase() {
-  const [selectedKey, setSelectedKey] = useState("software")
-  const [expandedKeys, setExpandedKeys] = useState<string[]>([])
-
-  const findLabelByKey = (key: string): string => {
-    for (const item of menuItems) {
-      if (item.key === key) {
-        return item.label;
-      }
-
-      if (item.children) {
-        const child = item.children.find(child => child.key === key);
-        if (child) {
-          return child.label;
-        }
-      }
-    }
-
-    return key;
-  };
+import ProductMenu from "@/components/products/ProductMenuClient"
+import {serverGet} from "@/utils/request";
+import AnimatedSection from "@/components/AnimatedSection";
+import {Suspense} from "react";
 
-  const products = [
-    {
-      id: 1,
-      title: "可视化数字孪生平台",
-      bgColor: "bg-gradient-to-br from-blue-900 via-blue-800 to-slate-800",
-    },
-    {
-      id: 2,
-      title: "水库雨水情测报与大坝安全监测平台",
-      bgColor: "bg-gradient-to-br from-teal-900 via-emerald-800 to-cyan-800",
-    },
-    {
-      id: 3,
-      title: "物联网平台",
-      bgColor: "bg-gradient-to-br from-teal-900 via-emerald-800 to-cyan-800",
-    },
-    {
-      id: 4,
-      title: "智慧档案库可视化平台",
-      bgColor: "bg-gradient-to-br from-blue-900 via-indigo-800 to-slate-800",
-    },
-    {
-      id: 5,
-      title: "智慧环保监测管理平台",
-      bgColor: "bg-gradient-to-br from-slate-900 via-blue-900 to-indigo-900",
-    },
-    {
-      id: 6,
-      title: "自然灾害应急能力提升平台",
-      bgColor: "bg-gradient-to-br from-slate-900 via-gray-800 to-blue-900",
-    },
-  ]
+export default async function ProductShowcase() {
 
-  const menuItems = [
-    {
-      key: "software",
-      label: "软件产品",
-    },
-    {
-      key: "hardware",
-      label: "硬件产品",
-      children: [
-        {
-          key: "sensor",
-          label: "传感设备",
-        },
-        {
-          key: "iot",
-          label: "物联设备",
-        },
-        {
-          key: "terminal",
-          label: "智能交互终端",
-        },
-      ],
+  const res = await serverGet<ProductCategory[]>("/webSite/getProductCategoryAndType",null,{
+    next: {
+      revalidate: 180
     },
-  ]
-
-  const handleMenuClick = (key: string) => {
-    setSelectedKey(key)
+    cache: "force-cache"
+  })
+  if(res.code !== 200){
+    return <div>服务器错误</div>
   }
+  const menuItems = res.data.map( (item) => ({
+    key: item.productCategoryName,
+    label: item.productCategoryName,
+    children: item.productTypes.map( (type) => ({
+      key: type.productTypeName,
+      label: type.productTypeName,
+    }))
+  }))
 
-  const toggleExpanded = (key: string) => {
-    setExpandedKeys((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]))
-  }
-
-  const DesktopMenu = () => (
-    <div className="space-y-1">
-      {menuItems.map((item) => (
-        <div key={item.key}>
-          <div
-            className={`flex items-center justify-between px-4 py-6 text-sm font-medium cursor-pointer rounded-lg transition-all duration-300 ${
-              selectedKey === item.key
-                ? "bg-blue-500 text-white shadow-lg"
-                : "text-gray-700 bg-blue-100 hover:bg-gray-100 hover:text-gray-900"
-            }`}
-            onClick={() => {
-              if (item.children) {
-                toggleExpanded(item.key)
-              } else {
-                handleMenuClick(item.key)
-              }
-            }}
-          >
-            <span className="transition-all duration-200">{item.label}</span>
-            {item.children && (
-              <svg
-                className={`w-4 h-4 transition-all duration-300 ease-in-out ${
-                  expandedKeys.includes(item.key) ? "rotate-90" : ""
-                } ${selectedKey === item.key ? "text-white" : "text-gray-500 hover:text-blue-400"}`}
-                fill="none"
-                stroke="currentColor"
-                viewBox="0 0 24 24"
-              >
-                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
-              </svg>
-            )}
-          </div>
-
-          {item.children && (
-            <div
-              className={`ml-4 mt-1 space-y-1 overflow-hidden transition-all duration-300 ease-in-out ${
-                expandedKeys.includes(item.key) ? "max-h-96 opacity-100" : "max-h-0 opacity-0"
-              }`}
-            >
-              {item.children.map((child) => (
-                <div
-                  key={child.key}
-                  className={`px-4 py-4 text-sm cursor-pointer rounded-lg transition-all duration-200 ${
-                    selectedKey === child.key
-                      ? "bg-blue-500 text-white shadow-md"
-                      : "text-gray-600 hover:bg-blue-50 hover:text-blue-700"
-                  }`}
-                  onClick={() => handleMenuClick(child.key)}
-                >
-                  <span className="flex items-center">{child.label}</span>
-                </div>
-              ))}
-            </div>
-          )}
-        </div>
-      ))}
-    </div>
-  )
 
-  const MobileMenu = () => (
-    <div className="bg-white shadow-sm">
-      <div className="px-4 py-3">
-        <div className="flex flex-wrap gap-2">
-          {menuItems.map((item) => (
-            <div key={item.key} className="relative">
-              <button
-                className={`px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
-                  selectedKey === item.key ||
-                  (item.children && item.children.some((child) => selectedKey === child.key))
-                    ? "bg-blue-500 text-white shadow-md"
-                    : "bg-gray-100 text-gray-700 hover:bg-gray-200"
-                }`}
-                onClick={() => {
-                  if (item.children) {
-                    toggleExpanded(item.key)
-                  } else {
-                    handleMenuClick(item.key)
-                  }
-                }}
-              >
-                <span className="flex items-center gap-1">
-                  {item.label}
-                  {item.children && (
-                    <svg
-                      className={`w-3 h-3 transition-transform duration-200 ${
-                        expandedKeys.includes(item.key) ? "rotate-180" : ""
-                      }`}
-                      fill="none"
-                      stroke="currentColor"
-                      viewBox="0 0 24 24"
-                    >
-                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
-                    </svg>
-                  )}
-                </span>
-              </button>
-
-              {item.children && expandedKeys.includes(item.key) && (
-                <div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-10 min-w-[160px]">
-                  {item.children.map((child) => (
-                    <button
-                      key={child.key}
-                      className={`w-full text-left px-4 py-3 text-sm transition-colors duration-200 first:rounded-t-lg last:rounded-b-lg ${
-                        selectedKey === child.key ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-50"
-                      }`}
-                      onClick={() => {
-                        handleMenuClick(child.key)
-                        setExpandedKeys([]) // Close dropdown after selection
-                      }}
-                    >
-                      {child.label}
-                    </button>
-                  ))}
-                </div>
-              )}
-            </div>
-          ))}
-        </div>
-      </div>
-    </div>
-  )
+  const products = res.data.flatMap( (item) => item.productTypes.flatMap( (type) => type.productCenters))
 
   return (
     <>
-      <div>
-        <Image src={"/assets/productions/1.png"} alt={"产品中心"} width={1920} height={1080} />
-      </div>
-
-      <div className="py-6 sm:py-10">
-        <MainTitle title={"产品中心"} />
-      </div>
-
-      <div className="sm:hidden">
-        <MobileMenu />
-      </div>
-
-      <div className="flex mt-6 sm:mt-10 mb-12 sm:mb-16 w-full sm:w-4/5 mx-auto px-4 sm:px-0">
-        <div className="hidden sm:block w-56 lg:w-64 p-4">
-          <DesktopMenu />
+      <AnimatedSection effect="slide" direction="left">
+        <div>
+          <Image src={"/assets/productions/1.png"} alt={"产品中心"} width={1920} height={1080}/>
         </div>
+      </AnimatedSection>
 
-        <div className="flex-1 p-2 sm:p-4 lg:p-8 sm:pt-8">
-          <div className="sm:hidden mt-3 mb-10">
-            <SubTitle title={findLabelByKey(selectedKey)} customStyle={{textAlign:"center"}} />
-          </div>
-          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
-            {products.map((product) => (
-              <div
-                key={product.id}
-                className="bg-white rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-[1.02]"
-              >
-                <div className={`h-32 sm:h-40 lg:h-48 ${product.bgColor} relative overflow-hidden`}>
-                  <div className="absolute inset-3 sm:inset-4 space-y-2">
-                    <div className="flex space-x-2">
-                      <div className="w-12 sm:w-16 h-1.5 sm:h-2 bg-white/20 rounded"></div>
-                      <div className="w-8 sm:w-12 h-1.5 sm:h-2 bg-white/20 rounded"></div>
-                      <div className="w-14 sm:w-20 h-1.5 sm:h-2 bg-white/20 rounded"></div>
-                    </div>
-
-                    <div className="grid grid-cols-2 gap-2 mt-3 sm:mt-4">
-                      <div className="h-12 sm:h-16 bg-white/10 rounded border border-white/20"></div>
-                      <div className="h-12 sm:h-16 bg-white/10 rounded border border-white/20"></div>
-                    </div>
-
-                    <div className="mt-3 sm:mt-4 space-y-1">
-                      <div className="w-full h-0.5 sm:h-1 bg-white/20 rounded"></div>
-                      <div className="w-3/4 h-0.5 sm:h-1 bg-white/20 rounded"></div>
-                      <div className="w-1/2 h-0.5 sm:h-1 bg-white/20 rounded"></div>
-                    </div>
-
-                    <div className="absolute top-1 sm:top-2 right-1 sm:right-2 w-6 sm:w-8 h-6 sm:h-8 border border-white/30 rounded-full"></div>
-                    <div className="absolute bottom-1 sm:bottom-2 left-1 sm:left-2 w-3 sm:w-4 h-3 sm:h-4 bg-cyan-400/60 rounded"></div>
-                    <div className="absolute bottom-1 sm:bottom-2 right-1 sm:right-2 w-4 sm:w-6 h-4 sm:h-6 bg-blue-400/60 rounded"></div>
-                  </div>
-                </div>
-
-                <div className="p-4 sm:p-6 text-center">
-                  <h3 className="text-sm sm:text-base lg:text-lg font-medium text-gray-900 mb-3 sm:mb-4 leading-tight min-h-[2.5rem] sm:min-h-[3rem] flex items-center justify-center">
-                    {product.title}
-                  </h3>
-                  <button className="bg-blue-500 hover:bg-blue-600 text-white px-4 sm:px-6 py-2 sm:py-2.5 rounded-lg font-medium transition-all duration-200 text-sm sm:text-base hover:scale-105 active:scale-95">
-                    了解详情
-                  </button>
-                </div>
-              </div>
-            ))}
-          </div>
-        </div>
+      <div className="py-6 sm:py-10">
+        <MainTitle title={"产品中心"}/>
       </div>
+      <AnimatedSection effect="slide" direction="right">
+        <Suspense fallback={<div>加载中...</div>}>
+          <ProductMenu menuItems={menuItems} products={products} />
+        </Suspense>
+      </AnimatedSection>
     </>
   )
 }

+ 69 - 0
src/app/solutions/[id]/page.tsx

@@ -0,0 +1,69 @@
+import React from 'react';
+import {Breadcrumb} from "antd";
+import {serverGet} from "@/utils/request";
+import './rich.css'
+import MainTitle from "@/components/MainTitle";
+import ContentNotFound from "@/components/ContentNotFound";
+
+export const dynamicParams = true
+
+export const generateStaticParams = async () => {
+  const res = await serverGet<Page<Solution>, { pageNum: number; pageSize: number }>(
+    "/webSite/getSolution",
+    {pageNum: 1, pageSize: 20}, {
+      next: {
+        revalidate: 1800
+      },
+      cache: "force-cache"
+    }
+  )
+  return res.data.records.map((item) => ({
+    id: item.id,
+  }))
+}
+
+async function Page({
+                      params,
+                    }: {
+  params: Promise<{ id: string }>
+}) {
+  const {id} = await params;
+  const res = await serverGet<Solution, { id: string }>(
+    "/webSite/getSolutionById",
+    {id}, {
+      next: {
+        revalidate: 1800
+      },
+      cache: "force-cache"
+    }
+  )
+  if (!res.data) {
+    return <ContentNotFound />
+  }
+  return (
+    <>
+      <div className="w-4/5 mx-auto">
+        <div className="pt-5 sm:pt-10 sm:ml-20 flex gap-2">
+          <span className="text-sm">您当前的所在位置:</span>
+          <Breadcrumb
+            separator=">"
+            items={[
+              {title: "解决方案列表", href: "/solutions"},
+              {title: res.data.programName || "解决方案"},
+            ]}
+          />
+        </div>
+      </div>
+      <div className="py-6 sm:py-10">
+        <MainTitle title={"解决方案"} titleLetter={"SOLUTION_DETAIL"}/>
+      </div>
+      <div
+        dangerouslySetInnerHTML={{__html: res.data.programDetails as string}}
+        className="ql-editor w-9/10 sm:w-7/10 mx-auto sm:px-20 sm:py-10"
+      />
+    </>
+  );
+}
+
+
+export default Page;