Bladeren bron

feat(products): 实现产品菜单组件并优化软件简介展示

- 新增 ProductMenuClient 组件,支持桌面端和移动端菜单展示
-为 ProductionSoft 组件添加 softIntroduction 属性,动态展示软件简介- 更新“了解更多”链接路径,指向具体产品分类页面- 实现菜单项点击与展开逻辑,支持根据 URL 参数自动选中菜单
- 添加产品类型筛选功能,根据选中菜单动态展示对应产品列表
- 优化菜单样式与交互,提升用户体验与响应式适配
nahida 7 maanden geleden
bovenliggende
commit
7db3a1eb7b
2 gewijzigde bestanden met toevoegingen van 300 en 4 verwijderingen
  1. 4 4
      src/components/ProductionSoft.tsx
  2. 296 0
      src/components/products/ProductMenuClient.tsx

+ 4 - 4
src/components/ProductionSoft.tsx

@@ -3,7 +3,7 @@ import SubTitle from "@/components/subTitle";
 import Image from "next/image";
 import Link from 'next/link';
 
-function ProductionSoft() {
+function ProductionSoft({softIntroduction}: {softIntroduction: string}) {
   return (
     <div className={"w-full mx-auto sm:w-4/5"}>
       <div className="hidden lg:block">
@@ -15,11 +15,11 @@ function ProductionSoft() {
           <div>
             <h2 className="text-2xl font-bold mt-10 border-b-2 border-blue-400 pb-2 inline-block">软件简介</h2>
             <p className="text-white text-2xl leading-relaxed mb-8 mt-10 tracking-widest font-medium">
-              软件产品包括:物联网智慧园区综合平台、物联网智慧园区综合平台、自然灾害应急能力提升平台、物联网环保监测管理平台、水库雨水情测报与大坝安全监测平台等等......
+              {softIntroduction}
             </p>
           </div>
           <Link
-            href={"/"}
+            href={"/products?keyword=软件产品"}
             className="bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg font-medium transition-colors w-fit">
             了解更多
           </Link>
@@ -51,7 +51,7 @@ function ProductionSoft() {
           <Image className="mt-6" src={"/assets/home/16.png"} width={1000} height={1000} alt={"软件产品"} />
           <Image className="mt-6" src={"/assets/home/17.png"} width={1000} height={1000} alt={"软件产品"} />
           <Link
-            href={"/"}
+            href={"/products?keyword=软件产品"}
             className="mt-6 bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg font-medium transition-colors w-fit">
             了解更多
           </Link>

+ 296 - 0
src/components/products/ProductMenuClient.tsx

@@ -0,0 +1,296 @@
+"use client"
+
+import {useEffect, useMemo, useState} from "react"
+import SubTitle from "@/components/subTitle"
+import ProductGrid from "@/components/products/ProductGridClient"
+import {useSearchParams} from "next/navigation";
+
+interface MenuItem {
+  key: string
+  label: string
+  children?: MenuItem[]
+}
+
+interface ProductMenuProps {
+  menuItems: MenuItem[]
+  products: ProductItem[]
+}
+
+export default function ProductMenu({ menuItems, products }: ProductMenuProps) {
+  // console.log(products);
+  const params = useSearchParams();
+  let baseSelect = '';
+  if(params.get('keyword')){
+    baseSelect = params.get('keyword') || '';
+    if(Object.is(params.get('keyword'),"软件产品")){
+      if (menuItems.filter((item) => Object.is(item.key, "软件产品")).length > 0) {
+        baseSelect = menuItems[0].children?.[0].key || '';
+      }
+    }else if(Object.is(params.get('keyword'),"硬件产品")){
+      if (menuItems.filter((item) => Object.is(item.key, "硬件产品")).length > 0) {
+        baseSelect = menuItems[1].children?.[0].key || '';
+      }
+    }
+  }else if(menuItems[0]?.children){
+    if(menuItems[0]?.children?.length>0){
+      baseSelect = menuItems[0].children?.[0].key || '';
+    }
+  }else {
+    baseSelect = menuItems[0]?.key || '';
+  }
+  const [selectedKey, setSelectedKey] = useState(baseSelect)
+  const [expandedKeys, setExpandedKeys] = useState<string[]>([])
+  const showProductList = useMemo(
+    () => products.filter((product) => product.productType === selectedKey),
+    [products, selectedKey]
+  );
+
+  useEffect(() => {
+    if(window.innerWidth > 640){
+      for (const item of menuItems) {
+        if (item.children) {
+          const child = item.children.find((child) => child.key === baseSelect)
+          if (child) {
+            // console.log(child)
+            setExpandedKeys([item.key])
+            break
+          }
+        }
+      }
+    }
+  }, [menuItems, baseSelect])
+
+  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
+  }
+
+  const handleMenuClick = (key: string) => {
+    setSelectedKey(key)
+    const urlSearchParams = new URLSearchParams(params.toString());
+    urlSearchParams.set('keyword', key);
+    window.history.replaceState({}, '', `?${urlSearchParams.toString()}`);
+
+    // 查找包含这个子菜单的父菜单并自动展开
+    for (const item of menuItems) {
+      if (item.children) {
+        const child = item.children.find((child) => child.key === key)
+        if (child) {
+          // 如果找到子菜单,自动展开父菜单
+          setExpandedKeys((prev) => {
+            if (prev.includes(item.key)) {
+              return prev
+            }
+            return [...prev, item.key]
+          })
+          break
+        }
+      }
+    }
+  }
+
+  const toggleExpanded = (key: string) => {
+    setExpandedKeys((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]))
+  }
+
+  return (
+    <>
+      <div className="sm:hidden">
+        <MobileMenu
+          menuItems={menuItems}
+          selectedKey={selectedKey}
+          expandedKeys={expandedKeys}
+          onMenuClick={handleMenuClick}
+          onToggleExpanded={toggleExpanded}
+        />
+      </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
+            menuItems={menuItems}
+            selectedKey={selectedKey}
+            expandedKeys={expandedKeys}
+            onMenuClick={handleMenuClick}
+            onToggleExpanded={toggleExpanded}
+          />
+        </div>
+
+        <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>
+          <ProductGrid products={showProductList} />
+        </div>
+      </div>
+    </>
+  )
+}
+
+function DesktopMenu({
+                       menuItems,
+                       selectedKey,
+                       expandedKeys,
+                       onMenuClick,
+                       onToggleExpanded,
+                     }: {
+  menuItems: MenuItem[]
+  selectedKey: string
+  expandedKeys: string[]
+  onMenuClick: (key: string) => void
+  onToggleExpanded: (key: string) => void
+}) {
+  return (
+    <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 ||
+              (item.children && item.children.some((child) => selectedKey === child.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) {
+                onToggleExpanded(item.key)
+              } else {
+                onMenuClick(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 ||
+                  (item.children && item.children.some((child) => selectedKey === child.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={() => onMenuClick(child.key)}
+                >
+                  <span className="flex items-center">{child.label}</span>
+                </div>
+              ))}
+            </div>
+          )}
+        </div>
+      ))}
+    </div>
+  )
+}
+
+function MobileMenu({
+                      menuItems,
+                      selectedKey,
+                      expandedKeys,
+                      onMenuClick,
+                      onToggleExpanded,
+                    }: {
+  menuItems: MenuItem[]
+  selectedKey: string
+  expandedKeys: string[]
+  onMenuClick: (key: string) => void
+  onToggleExpanded: (key: string) => void
+}) {
+  return (
+    <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) {
+                    onToggleExpanded(item.key)
+                  } else {
+                    onMenuClick(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={() => {
+                        onMenuClick(child.key)
+                        onToggleExpanded(item.key) // Close dropdown after selection
+                      }}
+                    >
+                      {child.label}
+                    </button>
+                  ))}
+                </div>
+              )}
+            </div>
+          ))}
+        </div>
+      </div>
+    </div>
+  )
+}