Jelajahi Sumber

个人中心订单相关和首页搜索

zhangzf 2 minggu lalu
induk
melakukan
a09ae1c967

+ 110 - 12
src/app/(public)/customer/account/_components/AccountVipPopup.tsx

@@ -2,9 +2,13 @@
 // import Link from "next/link";
 // 轮播逻辑
 import { Swiper, SwiperSlide } from "swiper/react";
-import { Navigation } from "swiper/modules";
+import { Navigation, Scrollbar } from "swiper/modules";
+import { Swiper as SwiperType } from "swiper/types";
+import { useEffect, useState } from "react";
+import Image from "next/image";
 import "swiper/css";
 import "swiper/css/navigation";
+import { clientFetch } from "@/lib/restApiClient";
 
 // 模拟你的每一页数据,方便维护
 const vipList = [
@@ -109,6 +113,7 @@ const vipList = [
     ],
   },
 ];
+
 export default function AccountVipPopup({
   visible,
   onClose,
@@ -118,7 +123,58 @@ export default function AccountVipPopup({
 }) {
   // 如果不显示,直接 return null
   if (!visible) return null;
-
+  // swiper实例
+  const [swiperRef, setSwiperRef] = useState<SwiperType | null>(null);
+  // 当前slide下标
+  const [activeIndex, setActiveIndex] = useState(0);
+  // 滑动切换触发
+  const handleSlideChange = (swiper: SwiperType) => {
+    setActiveIndex(swiper.activeIndex);
+    console.log("aaa slide huadongle ");
+  };
+  const slideItem = vipList[activeIndex];
+  const slideLevelNum = slideItem?.level ?? 1;
+  const [loading, setLoading] = useState(true);
+  const [VipGrothData, setVipGrothData] = useState<any>(null);
+  useEffect(() => {
+    const fetchGrothValue = async () => {
+      try {
+        setLoading(true);
+        const res = await clientFetch("/api/customer/growth-value");
+        if (res.data.success) {
+          setVipGrothData(res.data.data);
+          console.log("vip成长值数据:", res.data.data);
+        }
+      } catch (error) {
+        console.error("获取vip成长值失败:", error);
+      } finally {
+        setLoading(false);
+      }
+    };
+    fetchGrothValue();
+  }, []);
+  const progressPercent = VipGrothData
+    ? Math.min(
+        Math.max(
+          (VipGrothData.growth_value /
+            VipGrothData.next_level.min_growth_value) *
+            100,
+          0,
+        ),
+        100,
+      )
+    : 0;
+  const userCurrentLevelNum =
+    Number(VipGrothData?.current_level.name.replace(/[^0-9]/g, "")) || 0;
+  let showText = "";
+  console.log("userCurrentLevelNum", userCurrentLevelNum, slideLevelNum);
+  if (userCurrentLevelNum === slideLevelNum) {
+    showText = `Growth Value ${VipGrothData?.growth_value}/${VipGrothData?.next_level.min_growth_value}`;
+  } else if (userCurrentLevelNum > slideLevelNum) {
+    showText = "aaa";
+  } else {
+    showText = `Integral to 2000`;
+  }
   return (
     // 全屏遮罩
     <div className="fixed inset-0 z-50 bg-black/50">
@@ -157,14 +213,17 @@ export default function AccountVipPopup({
     bg-[length:100%_100%]"
               >
                 <div className="absolute top-0.75 left-8 text-4.5 text-[#FFD1B9] font-semibold">
-                  Vip <span className="levelcard">4</span>
+                  Vip <span className="levelcard">{userCurrentLevelNum}</span>
                 </div>
                 <div className="flex flex-col justify-between items-center text-white">
                   <div className="w-18 h-18 mt-6">
-                    <img
-                      className="max-w-full border-0 align-top"
-                      src="https://cdn.alipearlhair.com/media/wysiwyg/newap/202303131637.png "
+                    <Image
+                      src="https://cdn.alipearlhair.com/media/wysiwyg/newap/202303131637.png"
                       alt=""
+                      width={0}
+                      height={0}
+                      sizes="100vw"
+                      className="max-w-full border-0 align-top w-full h-auto"
                     />
                   </div>
                   <p className="text-4.5 mt-3 font-semibold"></p>
@@ -178,19 +237,31 @@ export default function AccountVipPopup({
                 <div className="box-border w-full mt-10.5 mb-9.75">
                   <div className="text-center text-sm text-white mb-3">
                     <span className="account-center-progress-text-pre">
-                      Growth Value 100309/500001
+                      {showText}
                     </span>
                   </div>
                   <div className="relative h-1 bg-[#5F4040]">
-                    <div className="w-4 h-4 rounded-full bg-black absolute top-[-0.1867rem] left-0 block"></div>
-                    <div className="rounded-[1.2821vw] w-[35%] h-full bg-[rgba(255,255,255,1)]"></div>
+                    <div
+                      className="w-4 h-4 rounded-full bg-black absolute top-1/2 -translate-y-1/2  block"
+                      style={{ left: `${progressPercent}%` }}
+                    ></div>
+                    <div
+                      className="rounded-[1.2821vw] h-full bg-[rgba(255,255,255,1)]"
+                      style={{ width: `${progressPercent}%` }}
+                    ></div>
                   </div>
                   {/* 轮播 */}
                   <div className="swiper-container-center vip-swiper">
                     <Swiper
-                      modules={[Navigation]}
-                      navigation // 开启左右箭头
+                      modules={[Navigation, Scrollbar]}
                       slidesPerView={1}
+                      navigation={{
+                        prevEl: ".btn-prev", // 左箭头class
+                        nextEl: ".btn-next", // 右箭头class
+                        disabledClass: "opacity-20", // 滑到首尾自动加这个类,Tailwind直接用
+                      }}
+                      onSwiper={(swiper) => setSwiperRef(swiper)}
+                      onSlideChange={handleSlideChange}
                       spaceBetween={0}
                       className="swiper-container-horizontal"
                     >
@@ -203,7 +274,7 @@ export default function AccountVipPopup({
                           data-level={item.level}
                         >
                           {/* 标题 */}
-                          <h3 className="text-lg font-medium mb-4">
+                          <h3 className="text-lg font-medium mb-4 mt-4">
                             {item.title}
                           </h3>
 
@@ -229,6 +300,33 @@ export default function AccountVipPopup({
                           </div>
                         </SwiperSlide>
                       ))}
+                      {/* 自定义左右按钮,放在Swiper内部,同级slide */}
+                      <button className="btn-prev absolute left-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 rounded-full bg-black/50 shadow flex items-center justify-center">
+                        {/* 自定义左箭头SVG */}
+                        <svg
+                          width="16"
+                          height="16"
+                          viewBox="0 0 24 24"
+                          fill="none"
+                          stroke="#fff"
+                          stroke-width="2"
+                        >
+                          <path d="M15 18l-6-6 6-6" />
+                        </svg>
+                      </button>
+                      <button className="btn-next absolute right-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 rounded-full bg-black/50 shadow flex items-center justify-center">
+                        {/* 自定义右箭头SVG */}
+                        <svg
+                          width="16"
+                          height="16"
+                          viewBox="0 0 24 24"
+                          fill="none"
+                          stroke="#fff"
+                          stroke-width="2"
+                        >
+                          <path d="M9 18l6-6-6-6" />
+                        </svg>
+                      </button>
                     </Swiper>
                   </div>
                 </div>

+ 122 - 41
src/app/(public)/customer/order/history/_components/OrderHistory.tsx

@@ -2,7 +2,7 @@
 import { useEffect, useState, useRef, useCallback } from "react";
 import { useRouter } from "next/navigation";
 import Link from "next/link";
-import { useSearchParams } from "next/navigation";
+import { clientFetch } from "@/lib/restApiClient";
 
 // 模拟订单数据
 const mockOrders = [
@@ -15,7 +15,7 @@ const mockOrders = [
         name: "Highlight Wigs For Women Headband Wigs with Blonde Highlights",
         specs: "10, #022, Straight, 5 Free Headbands",
         price: "88 points + $154.05x1",
-        image:"",
+        image: "",
       },
     ],
     progress: "Preparing Order",
@@ -30,7 +30,7 @@ const mockOrders = [
         name: "Alipearl Lace Front Wig 180% Density Body Wave Human Hair Wigs Pre...",
         specs: "10+180%, Medium, No Adjustable Band",
         price: "$128.82x1",
-        image:"",
+        image: "",
       },
     ],
     progress: "Preparing Order",
@@ -45,7 +45,7 @@ const mockOrders = [
         name: "3 Bundles Deep Wave Hair With 4*4 Lace Closure Alipearl Brazilian Hair",
         specs: "8 8 8, Free Part, 8",
         price: "$142.14x1",
-        image:"",
+        image: "",
       },
       {
         name: "Princess Braids Wig Blonde Barbie Pre-Styled Highlight Wig 13×4 Front Wig",
@@ -58,7 +58,7 @@ const mockOrders = [
         name: "Deep Wave Wig 100 Human Hair Swiss Lace Wig Lace Front Wig",
         specs: "10, Medium, 100%",
         price: "$252.00x1",
-         image:"",
+        image: "",
       },
       {
         name: "Braids Wig Blonde Barbie Pre-Styled Highlight Wig 13×4 Front Wig",
@@ -71,7 +71,7 @@ const mockOrders = [
         name: "random free wigs-2",
         specs: "",
         price: "$0.00x1",
-        image:"",
+        image: "",
       },
     ],
     progress: "Preparing Order",
@@ -85,64 +85,129 @@ const tabs = [
   { key: "all", label: "All" },
   { key: "processing", label: "Processing" },
   { key: "complete", label: "Complete" },
-  { key: "Payment", label: "Payment Review" }, // 补充模拟标签,测试滚动
+  { key: "pending_payment", label: "Payment Review" }, // 补充模拟标签,测试滚动
 ];
 
 export default function OrderHistory() {
+  const [loadMoreLoading, setLoadMoreLoading] = useState(false);
+  const [page, setPage] = useState(1);
+  const [total, setTotal] = useState(0);
+  const PAGE_SIZE = 10;
+  // 是否还有下一页
+  const hasMore = page * PAGE_SIZE < total;
+  //  防抖定时器ref
+  const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
+  const DEBOUNCE_DELAY = 500; // 防抖延迟毫秒
+  const sentinelRef = useRef<HTMLDivElement>(null);
+  const listWrapRef = useRef<HTMLDivElement>(null);
   const router = useRouter();
   const [activeTab, setActiveTab] = useState("all");
-  const [filteredOrders, setFilteredOrders] = useState(mockOrders);
-  // 组件内新增
-  const searchParams = useSearchParams();
+  const [filteredOrders, setFilteredOrders] = useState<any[]>([]);
   const lastHashRef = useRef<string>("");
-  // 模拟接口请求:不同标签传不同参数
-  const fetchOrders = (tabKey: string) => {
-    console.log("请求接口参数:", tabKey); // 实际项目替换为真实接口调用
-    // 这里简化逻辑,仅过滤Processing状态(实际接口返回对应数据)
-    if (tabKey === "processing") {
-      setFilteredOrders(mockOrders.filter((o) => o.status === "Processing"));
-    } else if (tabKey === "complete") {
-      setFilteredOrders([]); // 模拟无完成订单
-    } else {
-      setFilteredOrders(mockOrders);
+  const fetchOrders = async (tabKey: string, isLoadMore = false) => {
+    if (loadMoreLoading) return;
+    setLoadMoreLoading(true);
+    console.log("请求接口参数:", tabKey, page, isLoadMore);
+    try {
+      const resp = await clientFetch(
+        `/api/customer/orders?page=${page}&limit=${PAGE_SIZE}&status=${tabKey}`,
+      );
+      let records = resp.data.records ?? [];
+      setTotal(resp.data.total ?? 0);
+      // 模拟数据后面换成真正的接口数据
+      setTotal(100);
+      records = mockOrders; //
+      if (isLoadMore) {
+        setFilteredOrders((prev) => [...prev, ...records]);
+      } else {
+        setFilteredOrders(records);
+        setPage(1);
+        listWrapRef.current?.scrollTo({ top: 0 });
+      }
+    } catch (err) {
+      console.error("请求失败", err);
+    } finally {
+      setLoadMoreLoading(false);
     }
   };
-  // 把更新状态+请求的逻辑抽离,Effect 只调用这个函数,不直接写setState
+
   const updateTabByHash = useCallback(
     (hashKey: string) => {
+      console.log("执行切换tab逻辑", hashKey);
       const validTab = tabs.find((t) => t.key === hashKey.toLowerCase());
       const targetKey = validTab ? validTab.key : "all";
-      // setState 全部放在回调函数里,不在 useEffect 顶层同步执行
-      setActiveTab(targetKey);
-      fetchOrders(targetKey);
+      // setActiveTab(targetKey);
+      fetchOrders(targetKey, false);
     },
-    [tabs, fetchOrders],
+    [tabs],
   );
-
-  // 监听哈希变化,Effect 内部无任何直接setState
   useEffect(() => {
-    const currentHash = searchParams.get("") || "all";
-    // 对比上一次缓存的hash,避免重复执行
-    if (lastHashRef.current !== currentHash) {
-      lastHashRef.current = currentHash;
-      // 仅调用抽离好的函数,Effect内部没有setActiveTab
-      updateTabByHash(currentHash);
+    const getHash = () =>
+      window.location.hash.replace("#", "").toLowerCase() || "all";
+    const hash = getHash();
+    if (lastHashRef.current !== hash) {
+      lastHashRef.current = hash;
+      updateTabByHash(hash);
     }
-  }, [searchParams, updateTabByHash]);
+  }, [updateTabByHash]);
+
+  // 专门用来销毁定时器,防止内存泄漏,依赖空数组只执行一次挂载/卸载
+  useEffect(() => {
+    return () => {
+      if (debounceTimerRef.current) {
+        clearTimeout(debounceTimerRef.current);
+      }
+    };
+  }, []);
+  useEffect(() => {
+    const sentinel = sentinelRef.current;
+    const wrap = listWrapRef.current;
+    if (!sentinel || !wrap) return;
+
+    const observer = new IntersectionObserver(
+      (entries) => {
+        const entry = entries[0];
+        if (entry.isIntersecting && hasMore && !loadMoreLoading) {
+          const nextPage = page + 1;
+          setPage(nextPage);
+          fetchOrders(activeTab, true);
+        }
+      },
+      { root: wrap, rootMargin: "200px 0px 0px 0px" },
+    );
 
+    observer.observe(sentinel);
+    return () => observer.disconnect();
+  }, [activeTab, hasMore, loadMoreLoading, page]);
   // 点击标签切换
   const handleTabClick = (tabKey: string) => {
-    // window.location.hash = tabKey; // 更新URL哈希
-    router.push(`#${tabKey}`, { scroll: false });
+    // 点击立刻更新激活态,按钮样式实时切换
     setActiveTab(tabKey);
-    fetchOrders(tabKey);
+
+    // 清除上一次防抖任务
+    if (debounceTimerRef.current) {
+      clearTimeout(debounceTimerRef.current);
+    }
+
+    // 防抖:延迟更新URL + 请求接口
+    debounceTimerRef.current = setTimeout(() => {
+      const hash = tabKey.toLowerCase();
+      if (lastHashRef.current !== hash) {
+        lastHashRef.current = hash;
+        router.push(`#${tabKey}`, { scroll: false });
+        updateTabByHash(hash); // 仅负责调接口
+      }
+    }, DEBOUNCE_DELAY);
   };
 
   return (
     <>
       <div className="min-h-screen bg-gray-50">
         {/* 顶部返回栏 + 标题 */}
-        <Link href={"/customer/account"} className="sticky top-0 z-20 bg-white px-4 py-3 border-b flex justify-between items-center w-full">
+        <Link
+          href={"/customer/account"}
+          className="sticky top-0 z-20 bg-white px-4 py-3 border-b flex justify-between items-center w-full"
+        >
           <button className="text-black text-xl mr-4">
             <svg
               className="w-6 h-6"
@@ -184,8 +249,8 @@ export default function OrderHistory() {
         </div>
 
         {/* 订单列表 */}
-        <div className="px-4 py-3">
-          {filteredOrders.length === 0 ? (
+        <div className="px-4 py-3  h-[72vh] overflow-y-auto" ref={listWrapRef}>
+          {filteredOrders.length === 0 && !loadMoreLoading ? (
             <div className="text-center py-8 text-gray-500">
               No orders found
             </div>
@@ -239,7 +304,7 @@ export default function OrderHistory() {
                 {/* 订单进度条 */}
                 <Link
                   href={`/customer/order/track/order_id/${order.id}`}
-                  className="mb-4 h-8 bg-gray-100 rounded-full h-2 flex items-center px-2"
+                  className="mb-4  bg-gray-100 rounded-full h-8 flex items-center px-2"
                 >
                   <span className="ml-2 text-xs text-gray-700 flex-1 flex justify-between">
                     {order.progress}
@@ -300,6 +365,22 @@ export default function OrderHistory() {
               </div>
             ))
           )}
+          {/* 无限滚动哨兵:监听触底 */}
+          <div ref={sentinelRef} className="h-1"></div>
+
+          {/* 加载更多 loading */}
+          {loadMoreLoading && (
+            <div className="text-center py-4 text-gray-500">
+              Loading more orders...
+            </div>
+          )}
+
+          {/* 无下一页提示 */}
+          {!hasMore && filteredOrders.length > 0 && !loadMoreLoading && (
+            <div className="text-center py-4 text-gray-400 text-sm">
+              No more orders
+            </div>
+          )}
         </div>
       </div>
     </>

+ 28 - 27
src/app/(public)/customer/order/view/order_id/[id]/_components/OrderInfoBlock.tsx

@@ -1,49 +1,50 @@
+// OrderInfoBlock.tsx
+import type { OrderData } from "@/types/api/customer/order/vieworder";
+
 interface Props {
-  data: {
-    shipTo: string;
-    orderNo: string;
-    status: string;
-    orderDate: string;
-    shippingMethod: string;
-    paymentMethod: string;
-    CustomerEmail:string;
-    paymentDetail: Record<string, string>;
-  };
+  orderData: OrderData; // 直接传入完整订单对象
 }
 
-export default function OrderInfoBlock({ data }: Props) {
+export default function OrderInfoBlock({ orderData }: Props) {
+  console.log("orderData:::",orderData);
+  
+  // 拼接收货信息
+  const shipToText = `${orderData.shipping_address.first_name} ${orderData.shipping_address.last_name}, 
+Phone: ${orderData.shipping_address.phone}, 
+${orderData.shipping_address.city}, ${orderData.shipping_address.state}, ${orderData.shipping_address.country} ${orderData.shipping_address.postcode}`;
+
   return (
     <>
       <h2 className="text-xl font-bold mb-6 border-b pb-3">Order Information</h2>
 
-      {/* 两列网格布局,所有行严格对齐,Payment区域不会错位 */}
+      {/* 两列网格布局 */}
       <div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-5">
         <div className="font-medium text-gray-700">Ship To:</div>
-        <div>{data.shipTo}</div>
+        <div>{shipToText}</div>
 
         <div className="font-medium text-gray-700">Order Number:</div>
-        <div>{data.orderNo}</div>
+        <div>{orderData.increment_id}</div>
 
         <div className="font-medium text-gray-700">Status:</div>
-        <div>{data.status}</div>
+        <div>{orderData.status}</div>
 
         <div className="font-medium text-gray-700">Order Date:</div>
-        <div>{data.orderDate}</div>
+        <div>{orderData.created_at}</div>
 
         <div className="font-medium text-gray-700">Customer Email:</div>
-        <div>{data.CustomerEmail}</div>
+        <div>{orderData.customer_email}</div>
+
         <div className="font-medium text-gray-700">Shipping Method:</div>
-        <div>{data.shippingMethod}</div>
+        <div>{orderData.shipping_title}</div>
+
         <div className="font-medium text-gray-700">Payment Method:</div>
-        <div className="bg-pink-300 px-3 py-1 rounded w-fit">Klarna</div>
-        {/* <div className="flex flex-col gap-2">
-          <span className="bg-pink-300 px-3 py-1 rounded w-fit">Klarna.</span>
-          <div className="space-y-1">
-            {Object.entries(data.paymentDetail).map(([key, val]) => (
-              <div key={key}>{key} {val}</div>
-            ))}
-          </div>
-        </div> */}
+        <div className="bg-pink-300 px-3 py-1 rounded w-fit">
+          {orderData.payment.method_title}
+        </div>
+
+        {/* 可选:显示总价信息 */}
+        <div className="font-medium text-gray-700">Total Amount:</div>
+        <div>{orderData.order_currency_code} {orderData.grand_total}</div>
       </div>
     </>
   );

+ 48 - 16
src/app/(public)/customer/order/view/order_id/[id]/_components/PriceSummary.tsx

@@ -1,39 +1,71 @@
-interface PriceData {
-  subtotal: string;
-  shipping: string;
-  insurance: string;
-  total: string;
-}
+// PriceSummary.tsx
+import type { OrderData } from "@/types/api/customer/order/vieworder";
 
 interface Props {
-  priceData: PriceData;
+  orderData: OrderData;
 }
 
-export default function PriceSummary({ priceData }: Props) {
+export default function PriceSummary({ orderData }: Props) {
+  const currency = orderData.order_currency_code;
+  const subtotal = orderData.sub_total;
+  const shipping = orderData.shipping_amount;
+  const grandTotal = orderData.grand_total;
+  // 接口无保险费用,设为0
+  const insurance = 0;
+
   return (
     <div className="mt-6">
       <div className="space-y-3 border-t pt-4">
         <div className="flex justify-between text-gray-700">
           <span>Subtotal</span>
-          <span>{priceData.subtotal}</span>
+          <span>
+            {currency} {subtotal}
+          </span>
         </div>
         <div className="flex justify-between text-gray-700">
           <span>Shipping & Handling</span>
-          <span>{priceData.shipping}</span>
-        </div>
-        <div className="flex justify-between text-gray-700">
-          <span>Insurance (Insurance for Lost)</span>
-          <span>{priceData.insurance}</span>
+          <span>
+            {currency} {shipping}
+          </span>
         </div>
+        {insurance > 0 && (
+          <div className="flex justify-between text-gray-700">
+            <span>Insurance (Insurance for Lost)</span>
+            <span>
+              {currency} {insurance}
+            </span>
+          </div>
+        )}
+        {/* 可额外增加 Tax / Discount 行 */}
+        {/* Tax:不等于0才显示 */}
+        {orderData.tax_amount !== 0 && (
+          <div className="flex justify-between text-gray-700">
+            <span>Tax</span>
+            <span>
+              {currency} {orderData.tax_amount}
+            </span>
+          </div>
+        )}
+        {/* Discount:不等于0才显示 */}
+        {orderData.discount_amount !== 0 && (
+          <div className="flex justify-between text-gray-700">
+            <span>Discount</span>
+            <span>
+              {currency} {orderData.discount_amount}
+            </span>
+          </div>
+        )}
       </div>
 
       {/* 底部总价栏,浅黄色背景 */}
       <div className="bg-amber-50 mt-4 py-3 px-2 rounded">
         <div className="flex justify-between items-center">
           <p className="text-lg font-bold">Grand Total</p>
-          <p className="text-lg font-bold">{priceData.total}</p>
+          <p className="text-lg font-bold">
+            {currency} {grandTotal}
+          </p>
         </div>
       </div>
     </div>
   );
-}
+}

+ 14 - 25
src/app/(public)/customer/order/view/order_id/[id]/_components/ProductList.tsx

@@ -1,40 +1,29 @@
-interface ProductItem {
-  name: string;
-  spec: string;
-  price: string;
-  qty: number;
-  image:string;
-}
+// ProductList.tsx
+import type { OrderData } from "@/types/api/customer/order/vieworder";
 
 interface Props {
-  items: ProductItem[];
+  orderItems: OrderData["items"];
+  currencyCode: string;
 }
 
-export default function ProductList({ items }: Props) {
+export default function ProductList({ orderItems, currencyCode }: Props) {
   return (
     <div className="mb-6 space-y-4">
-      {items.map((item, index) => (
-        <div className="flex items-center justify-between gap-3" key={index}>
-          {item.image ? (
-            <img
-              src={item.image}
-              alt={item.name}
-              className="w-22.5 h-23 object-cover rounded shrink-0"
-            />
-          ) : (
-            <div className="w-31 h-23 bg-gray-100 rounded shrink-0"></div>
-          )}
+      {orderItems.map((item, index) => (
+        <div className="flex items-center gap-3" key={item.id ?? index}>
+          {/* 暂无商品原图:用占位框 */}
+          <div className="w-24 h-24 bg-gray-100 rounded shrink-0"></div>
 
-          <div>
+          <div className="flex-1">
             <p className="font-medium">{item.name}</p>
-            <p className="text-sm text-gray-600 my-1">{item.spec}</p>
+            <p className="text-sm text-gray-600 my-1">SKU: {item.sku ?? "-"}</p>
             <div className="flex justify-between">
-              <span>{item.price}</span>
-              <span>X {item.qty}</span>
+              <span>{currencyCode} {item.price}</span>
+              <span>× {item.qty_ordered}</span>
             </div>
           </div>
         </div>
       ))}
     </div>
   );
-}
+}

+ 22 - 59
src/app/(public)/customer/order/view/order_id/[id]/page.tsx

@@ -2,67 +2,27 @@ import OrderHeader from "./_components/OrderHeader";
 import OrderInfoBlock from "./_components/OrderInfoBlock";
 import ProductList from "./_components/ProductList";
 import PriceSummary from "./_components/PriceSummary";
-
+import {restApiFetch} from "@utils/bagisto/index";
+import type {  OrderData,OrderApiResponse } from "@/types/api/customer/order/vieworder";
 type Params = {
   id: string;
 };
-
 export default async function OrderDetail({ params }: { params: Params }) {
-  // 拿到动态订单ID:211600
-  const orderId = params.id;
+  // 解包拿到真实订单ID
+  const realParams = await params;
+  let orderId = realParams.id;
+  orderId = '9';
+  console.log("orderId:", orderId);
+
+  // 2. 服务端 fetch 请求我们自己的 Next.js API
+  const res = await restApiFetch<OrderApiResponse>({
+    api:`/customer/orders/${orderId}`,
+    method:"GET",
+  }
+  );
 
-  // === 服务端请求订单接口,传入订单号 ===
-//   const res = await fetch(
-//     `${process.env.API_URL}/api/order/detail?id=${orderId}`,
-//     {
-//       next: { revalidate: 0 },
-//     },
-//   );
-  //   const orderData = await res.json();
-  console.log("orderId:",orderId);
-  const orderData = {
-    shipTo: "Zhang Zhenfei,\nAaaaaaaa, Zzzzzz, Alaska\n90602, United States",
-    orderNo: orderId, // 自动使用路由传入的订单号
-    status: "Processing",
-    orderDate: "March 6, 2026",
-    shippingMethod: "USPS/FedEx(2-7 Business Days)",
-    paymentMethod: "Klarna",
-    CustomerEmail:"18567768109@163.Com",
-    paymentDetail: {
-      Customer: "18567768109",
-      Email: "@163.Com",
-      "Payment Method": "PayoneerKlarna",
-      "Payment Status": "Processing",
-      "Order No": "100055676",
-      "Customer Firstname": "Zhang",
-      "Customer Lastname": "Zhenfei",
-    },
-    // 多商品数组,自动兼容1件/多件商品
-    items: [
-      {
-        name: "Human Hair Headband Wigs Curly Bob Wig Straight Hair Wigs",
-        spec: "8, Straight, 5 Free Headbands.",
-        price: "$114.11",
-        image:"",
-        qty: 1,
-      },
-      // 你可以取消注释,测试多商品渲染
-      /*
-      {
-        name: "Deep Wave Lace Front Human Hair Wig",
-        spec: "13x4 Lace, Medium Cap, 180% Density",
-        price: "$252.00",
-        qty: 2,
-      },
-      */
-    ],
-    price: {
-      subtotal: "$114.11",
-      shipping: "$0.00",
-      insurance: "$3.42",
-      total: "$117.53",
-    },
-  };
+  const orderData: OrderData = res.body.data;
+  console.log("父组件 orderData:", orderData, !!orderData); 
   return (
     <div className="bg-gray-50 min-h-screen">
       {/* 顶部固定导航栏(客户端组件:返回按钮) */}
@@ -70,7 +30,7 @@ export default async function OrderDetail({ params }: { params: Params }) {
 
       <div className="px-5 py-4">
         {/* 收货信息、支付信息区块 */}
-        <OrderInfoBlock data={orderData} />
+        <OrderInfoBlock orderData={orderData} />
 
         {/* 分享按钮区域 */}
         <div className="my-6">
@@ -84,10 +44,13 @@ export default async function OrderDetail({ params }: { params: Params }) {
         </div>
 
         {/* 商品列表:循环渲染,自动兼容1件或多件商品 */}
-        <ProductList items={orderData.items} />
+            <ProductList
+          orderItems={orderData.items}
+          currencyCode={orderData.order_currency_code}
+        />
 
         {/* 金额汇总 */}
-        <PriceSummary priceData={orderData.price} />
+        <PriceSummary orderData={orderData} />
       </div>
     </div>
   );

+ 35 - 32
src/app/(public)/search/page.tsx

@@ -4,11 +4,11 @@ import NotFound from "@/components/theme/search/not-found";
 import { isArray } from "@/utils/type-guards";
 import { GET_FILTER_PRODUCTS } from "@/graphql";
 import { GET_PRODUCTS, GET_PRODUCTS_PAGINATION } from "@/graphql";
-import { cachedGraphQLRequest, getFilterAttributes } from "@/utils/hooks/useCache";
 import {
-  generateMetadataForPage,
-  buildProductFilters,
-} from "@/utils/helper";
+  cachedGraphQLRequest,
+  getFilterAttributes,
+} from "@/utils/hooks/useCache";
+import { generateMetadataForPage, buildProductFilters } from "@/utils/helper";
 import SortOrder from "@/components/theme/filters/SortOrder";
 import { SortByFields } from "@/utils/constants";
 import MobileFilter from "@/components/theme/filters/MobileFilter";
@@ -30,7 +30,7 @@ export async function generateStaticParams() {
     const commonSearches = [""];
     const params = [];
     for (const query of commonSearches) {
-      const {data} = await cachedGraphQLRequest<ProductsResponse>(
+      const { data } = await cachedGraphQLRequest<ProductsResponse>(
         "search",
         GET_PRODUCTS,
         {
@@ -54,17 +54,18 @@ export async function generateStaticParams() {
         }
         params.push(pageParams);
         if (i < totalPages - 1) {
-          const {data: pageData} = await cachedGraphQLRequest<ProductsResponse>(
-            "search",
-            GET_PRODUCTS,
-            {
-              query: query,
-              first: itemsPerPage,
-              sortKey: "CREATED_AT",
-              reverse: true,
-              ...(cursor && { after: cursor }),
-            },
-          );
+          const { data: pageData } =
+            await cachedGraphQLRequest<ProductsResponse>(
+              "search",
+              GET_PRODUCTS,
+              {
+                query: query,
+                first: itemsPerPage,
+                sortKey: "CREATED_AT",
+                reverse: true,
+                ...(cursor && { after: cursor }),
+              },
+            );
           cursor = pageData?.products?.pageInfo?.endCursor;
         }
       }
@@ -138,16 +139,17 @@ export default async function SearchPage({
     dataPromise = (async () => {
       let currentAfterCursor: string | undefined = afterCursor;
       if (currentPage > 0 && !afterCursor) {
-        const {data: cursorData} = await cachedGraphQLRequest<ProductsResponse>(
-          "search",
-          GET_PRODUCTS_PAGINATION,
-          {
-            query: searchValue,
-            first: currentPage * itemsPerPage,
-            sortKey: selectedSort.sortKey,
-            reverse: selectedSort.reverse,
-          },
-        );
+        const { data: cursorData } =
+          await cachedGraphQLRequest<ProductsResponse>(
+            "search",
+            GET_PRODUCTS_PAGINATION,
+            {
+              query: searchValue,
+              first: currentPage * itemsPerPage,
+              sortKey: selectedSort.sortKey,
+              reverse: selectedSort.reverse,
+            },
+          );
         currentAfterCursor = cursorData?.products?.pageInfo?.endCursor;
       }
 
@@ -162,7 +164,7 @@ export default async function SearchPage({
     })();
   }
 
-  const [{data}, filterAttributes] = await Promise.all([
+  const [{ data }, filterAttributes] = await Promise.all([
     dataPromise,
     getFilterAttributes(),
   ]);
@@ -183,12 +185,13 @@ export default async function SearchPage({
 
         <SortOrder sortOrders={SortByFields} title="Sort by" />
       </div>
-      <div className="flex items-center justify-between gap-4 py-8 md:hidden  mx-auto w-full max-w-screen-2xl px-4 xss:px-7.5">
-        <MobileFilter filterAttributes={filterAttributes} />
-
-        <SortOrder sortOrders={SortByFields} title="Sort by" />
-      </div>
+      {isArray(products) ? (
+        <div className="flex items-center justify-between gap-4 py-8 md:hidden  mx-auto w-full max-w-screen-2xl px-4 xss:px-7.5">
+          <MobileFilter filterAttributes={filterAttributes} />
 
+          <SortOrder sortOrders={SortByFields} title="Sort by" />
+        </div>
+      ) : null}
       {!isArray(products) && (
         <NotFound
           msg={`${

+ 40 - 0
src/app/api/customer/growth-value/route.ts

@@ -0,0 +1,40 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+
+export async function GET(req: NextRequest) {
+  try {
+    const guestToken = getAuthToken(req);
+    const response = await restApiFetch<any>({
+      api: "/customer/growth-value",
+      method: "GET",
+      cache: "no-store",
+      guestToken,
+    });
+
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 }
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 46 - 0
src/app/api/customer/orders/[id]/route.ts

@@ -0,0 +1,46 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+type Params = Promise<{ id: string }>;
+export async function GET(req: NextRequest, { params }: { params: Params }) {
+  try {
+     const { id } = await params; // 解包拿到订单ID
+    const guestToken = getAuthToken(req);
+
+   const apiUrl = `/customer/orders/${id}`;
+
+    console.log("最终请求API地址:", apiUrl);
+
+    const response = await restApiFetch<any>({
+      api: apiUrl,
+      method: "GET",
+      cache: "no-store",
+      guestToken,
+    });
+
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 }
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 61 - 0
src/app/api/customer/orders/route.ts

@@ -0,0 +1,61 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+
+export async function GET(req: NextRequest) {
+  try {
+    const guestToken = getAuthToken(req);
+
+    // 👇 取出所有前端传过来的查询参数
+    const searchParams = req.nextUrl.searchParams;
+    const page = searchParams.get("page");
+    const limit = searchParams.get("limit");
+    const status = searchParams.get("status");
+
+    // 👇 构建查询参数数组(自动过滤空值)
+    const queryParts: string[] = [];
+    if (page) queryParts.push(`page=${page}`);
+    if (limit) queryParts.push(`limit=${limit}`);
+    if (status) queryParts.push(`status=${status}`);
+
+    // 👇 最终拼接 URL
+    let apiUrl = "/customer/orders"; 
+    if (queryParts.length > 0) {
+      apiUrl += "?" + queryParts.join("&");
+    }
+
+    console.log("最终请求API地址:", apiUrl);
+
+    const response = await restApiFetch<any>({
+      api: apiUrl,
+      method: "GET",
+      cache: "no-store",
+      guestToken,
+    });
+
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 }
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 3 - 3
src/components/cart/OpenCart.tsx

@@ -9,11 +9,11 @@ export default function OpenCart({
   quantity?: number | string;
 }) {
   return (
-    <div className="relative flex  items-center justify-center rounded-md border-0 lg:border border-solid border-neutral-200 dark:border-neutral-700 lg:h-11 lg:w-11">
+    <div className="relative flex  items-center justify-center rounded-md w-6 h-6 border-0 lg:border border-solid border-neutral-200 dark:border-neutral-700 lg:h-11 lg:w-11">
       <ShoppingCartIcon className={clsx("h-5 w-5", className)} />
-
+{/* -mr-2  */}
       {quantity ? (
-        <div className="absolute right-0 top-0 -mr-2 margin-t lg:-mt-2 h-4 w-4 rounded bg-blue-600 text-[11px] font-medium text-white">
+        <div className="absolute right-0 bottom-0 margin-t lg:-mt-2 w-3 h-3  rounded-full bg-[#FF4500FF]  text-[11px] text-center leading-3 font-medium text-white">
           {quantity}
         </div>
       ) : null}

+ 61 - 45
src/components/layout/navbar/index.tsx

@@ -1,7 +1,7 @@
 import Link from "next/link";
 import { Suspense } from "react";
-import Search from "./Search";
-import { SearchSkeleton } from "@/components/common/skeleton/SearchSkeleton";
+// import Search from "./Search";
+// import { SearchSkeleton } from "@/components/common/skeleton/SearchSkeleton";
 import LogoIcon from "@components/common/icons/LogoIcon";
 import MobileMenuTrigger from "./MobileMenuTrigger";
 import { CartAndUserActions } from "./CartAndUserActions";
@@ -14,61 +14,77 @@ import { cachedGraphQLRequest } from "@utils/hooks/useCache";
 import { TreeCategoriesResponse } from "@/types/theme/category-tree";
 
 export default async function Navbar() {
+  const { data } = await cachedGraphQLRequest<TreeCategoriesResponse>(
+    "category",
+    GET_TREE_CATEGORIES,
+    { parentId: 1 },
+  );
+
+  const categories = data?.treeCategories || [];
 
-  const {data} = await cachedGraphQLRequest<TreeCategoriesResponse>(
-      "category",
-      GET_TREE_CATEGORIES,
-      { parentId: 1 }
-    );
-  
-  
-    const categories = data?.treeCategories || [];
-  
-    const filteredCategories = categories
-      .filter((cat: any) => cat.id !== "1")
-      .map((cat: any) => {
-        const translation = cat.translation;
-        return {
-          id: cat.id,
-          name: translation?.name || "",
-          slug: translation?.slug || "",
-        };
-      })
-      .filter((item: any) => item.name && item.slug);
-  
-    const menuData = [
-      { id: "all", name: "All", slug: "" },
-      ...filteredCategories.slice(0, 3),
-    ];
-  
+  const filteredCategories = categories
+    .filter((cat: any) => cat.id !== "1")
+    .map((cat: any) => {
+      const translation = cat.translation;
+      return {
+        id: cat.id,
+        name: translation?.name || "",
+        slug: translation?.slug || "",
+      };
+    })
+    .filter((item: any) => item.name && item.slug);
+
+  const menuData = [
+    { id: "all", name: "All", slug: "" },
+    ...filteredCategories.slice(0, 3),
+  ];
 
   return (
     <NavbarErrorBoundary>
       <header className="sticky top-0 z-10">
-        <nav className="relative flex flex-col items-center justify-between gap-4 bg-neutral-50 p-4 dark:bg-neutral-900 md:flex-row lg:px-6 lg:py-4">
+        <nav className="relative flex flex-col items-center justify-between gap-4 bg-neutral-50 p-4 pt-3 pb-3 dark:bg-neutral-900 md:flex-row lg:px-6 lg:py-4">
           <div className="flex w-full items-center justify-between gap-0 sm:gap-4">
             {/* 1. THE STATIC SHELL (Visible Instantly) */}
-            <div className="flex max-w-fit gap-2 xl:gap-6">
-
+            <div className="flex items-center max-w-fit gap-5 xl:gap-6">
               {/* 2. STATIC HOLE: Categories (Suspended) */}
               <Suspense fallback={<NavigationSkeleton />}>
                 <MobileMenuTrigger menuData={menuData} />
               </Suspense>
-              <Link
-                className="flex-initial h-9 w-36.5"
-                href="/"
-                aria-label="Go to homepage"
-              >
-                <LogoIcon />
-              </Link>
-              
-            </div>
-
-            <div className="flex-1 justify-center flex">
-              <Suspense fallback={<SearchSkeleton />}>
-                <Search search={false} />
-              </Suspense>
+              <div className="flex-1 justify-center flex">
+                {/* <Suspense fallback={<SearchSkeleton />}>
+                  <Search search={false} />
+                </Suspense> */}
+                <Link className="w-6 h-6 flex items-center justify-center" href={"/search?q"}>
+                  <svg
+                    xmlns="http://www.w3.org/2000/svg"
+                    width="17.7802734375"
+                    height="18.7802734375"
+                    viewBox="0 0 17.7802734375 18.7802734375"
+                    fill="none"
+                  >
+                    <circle
+                      cx="8.75"
+                      cy="8.75"
+                      r="8"
+                      stroke="rgba(0, 0, 0, 1)"
+                      stroke-width="1.5"
+                    ></circle>
+                    <path
+                      stroke="rgba(0, 0, 0, 1)"
+                      stroke-width="1.5"
+                      d="M14.25 15.25L17.25 18.25"
+                    ></path>
+                  </svg>
+                </Link>
+              </div>
             </div>
+            <Link
+              className="flex-initial h-9 w-36.5"
+              href="/"
+              aria-label="Go to homepage"
+            >
+              <LogoIcon />
+            </Link>
 
             {/* 3. DYNAMIC HOLE: Auth & Cart (Blocked by Session/Cookies) */}
             <Suspense fallback={<ActionsSkeleton />}>

+ 95 - 0
src/types/api/customer/order/vieworder.ts

@@ -0,0 +1,95 @@
+// 订单详情整体类型
+interface AddressInfo {
+  first_name: string;
+  last_name: string;
+  email: string;
+  phone: string;
+  address1: string | null;
+  address2: string | null;
+  city: string;
+  state: string;
+  country: string;
+  postcode: string;
+  company: string;
+}
+
+interface OrderItem {
+  id: number;
+  name: string;
+  sku: string;
+  type: string;
+  qty_ordered: number;
+  qty_shipped: number;
+  qty_invoiced: number;
+  qty_canceled: number;
+  qty_refunded: number;
+  price: number;
+  total: number;
+  weight: number;
+  product_id: number;
+}
+
+interface InvoiceItem {
+  name: string;
+  qty: number;
+  price: number;
+  total: number;
+}
+
+interface Invoice {
+  id: number;
+  increment_id: string;
+  state: string;
+  email_sent: number;
+  total_qty: number;
+  sub_total: number;
+  grand_total: number;
+  created_at: string;
+  items: InvoiceItem[];
+}
+
+interface PaymentInfo {
+  method: string;
+  method_title: string;
+}
+
+export interface OrderData {
+  id: number;
+  increment_id: string;
+  status: string;
+  status_label: string;
+  customer_email: string;
+  customer_first_name: string;
+  customer_last_name: string;
+  channel_name: string;
+  order_currency_code: string;
+  sub_total: number;
+  shipping_amount: number;
+  tax_amount: number;
+  discount_amount: number;
+  grand_total: number;
+  total_qty_ordered: number;
+  shipping_method: string;
+  shipping_title: string;
+  billing_address: AddressInfo;
+  shipping_address: AddressInfo;
+  payment: PaymentInfo;
+  items: OrderItem[];
+  invoices: Invoice[];
+  shipments: any[];
+  refunds: any[];
+  comments: any[];
+  can_cancel: boolean;
+  can_reorder: boolean;
+  created_at: string;
+  updated_at: string;
+}
+
+export interface OrderApiResponse {
+  status: number;
+  body: {
+    success: boolean;
+    message: string;
+    data: OrderData;
+  };
+}