Ver código fonte

搜索+订单物流查询

zhangzf 1 semana atrás
pai
commit
7d2d776f36

Diferenças do arquivo suprimidas por serem muito extensas
+ 117 - 24
src/app/(public)/customer/order/track/order_id/[id]/_components/TrackDetail.tsx


+ 59 - 0
src/app/(public)/customer/order/track/order_id/[id]/_components/TrackModal.tsx

@@ -0,0 +1,59 @@
+export interface TrackRecord {
+  status: string;
+  location: string;
+  time: string;
+}
+
+
+interface TrackModalProps {
+  visible: boolean;
+  trackList: TrackRecord[];
+  onClose: () => void;
+}
+
+export default function TrackModal({ visible, trackList, onClose }: TrackModalProps) {
+  if (!visible) return null;
+
+  return (
+    <div
+      className="fixed inset-0 bg-black/60 flex items-center justify-center z-[9999]"
+      onClick={onClose}
+    >
+      <div
+        className="w-[min(90%,600px)] max-h-[80vh] bg-white rounded-lg flex flex-col"
+        onClick={(e) => e.stopPropagation()}
+      >
+        <div className="flex items-center justify-between px-5 py-4 border-b border-gray-200">
+          <h3 className="text-base font-semibold m-0">Package Tracking Info (USPS)</h3>
+          <button
+            onClick={onClose}
+            className="bg-transparent border-none text-xl cursor-pointer px-2 hover:text-gray-500 transition-colors"
+          >
+            ×
+          </button>
+        </div>
+
+        <div className="px-5 py-4 overflow-y-auto flex-1">
+          {trackList.length === 0 ? (
+            <p className="text-gray-500 text-center py-8">No logistics data available</p>
+          ) : (
+            trackList.map((item, idx) => (
+              <div key={idx} className="py-3">
+                <div className="flex justify-between gap-3 mb-1.5">
+                  <span className="font-medium">{item.status}</span>
+                  <span className="text-sm text-gray-600 whitespace-nowrap">{item.time}</span>
+                </div>
+                <div className="text-sm text-gray-500">
+                  Location: {item.location || "Unknown Hub"}
+                </div>
+                {idx !== trackList.length - 1 && (
+                  <div className="w-full h-px bg-gray-200 mt-3"></div>
+                )}
+              </div>
+            ))
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 55 - 55
src/app/(public)/customer/order/track/order_id/[id]/page.tsx

@@ -1,74 +1,74 @@
 import OrderHeader from "../../../view/order_id/[id]/_components/OrderHeader";
 import AccountSwiper from "@components/customer/accountSwiper";
-import TrackDetail from "./_components/TrackDetail"
+import TrackDetail from "./_components/TrackDetail";
+import { restApiFetch } from "@utils/bagisto/index";
+import type { OrderData,OrderApiResponse } from "@/types/api/customer/order/vieworder";
 type Params = {
   id: string;
 };
+interface OrderAddress {
+  first_name: string;
+  last_name: string;
+  phone: string;
+  address1: string | null;
+  address2: string | null;
+  city: string;
+  state: string;
+  country: string;
+  postcode: string;
+  company: string;
+}
+export interface AddressData {
+  name: string;
+  line1: string;
+  line2: string;
+}
+
+function formatOrderAddress(addr: OrderAddress): AddressData {
+  // 拼接姓名
+  const fullName = `${addr.first_name} ${addr.last_name}`;
 
+  // line1:街道 + 城市 + 州
+  const addrParts: string[] = [];
+  if (addr.address1) addrParts.push(addr.address1);
+  if (addr.address2) addrParts.push(addr.address2);
+  addrParts.push(`${addr.city}, ${addr.state}`);
+  const line1 = addrParts.join(", ");
+
+  // line2:邮编 + 国家
+  const line2 = `${addr.postcode}, ${addr.country}`;
+
+  return {
+    name: fullName,
+    line1,
+    line2,
+  };
+}
 export default async function TrackOrderDetail({ params }: { params: Params }) {
   // 拿到动态订单ID:211600
-  const orderId = params.id;
+  // 解包拿到真实订单ID
+  const realParams = await params;
+  let orderId = realParams.id;
+  orderId = "9";
+  console.log("orderId:", orderId);
 
-  // === 服务端请求订单接口,传入订单号 ===
-//   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",
-  //       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",
-  //   },
-  // };
+  // 2. 服务端 fetch 请求我们自己的 Next.js API
+  const res = await restApiFetch<OrderApiResponse>({
+    api: `/customer/orders/${orderId}`,
+    method: "GET",
+  });
+  const orderData: OrderData = res.body.data;
+  const shippingAddr: AddressData = formatOrderAddress(orderData.shipping_address);
+  console.log("父组件 orderData:", orderData, !!orderData);
   return (
     <div className="bg-gray-50 min-h-screen">
       {/* 顶部固定导航栏(客户端组件:返回按钮) */}
       <OrderHeader title="Track Order" />
 
       <div className="px-5 py-4">
-        <TrackDetail />
+        <TrackDetail address={shippingAddr} id={orderId} />
       </div>
-         <AccountSwiper/>
+      <AccountSwiper />
     </div>
   );
 }

+ 44 - 0
src/app/(public)/search/_components/HotSearch.tsx

@@ -0,0 +1,44 @@
+'use client';
+
+import Link from 'next/link';
+import { createUrl } from '@/utils/helper';
+import { useSearchParams } from 'next/navigation';
+
+const hotTags = [
+  'HD Wig',
+  'Glueless Wig',
+  'New Arrival',
+  'Color Wig',
+  'Lace Front Wig',
+  'Bundles',
+  'M Hairline',
+  '13x6',
+];
+
+export default function HotSearch() {
+  const searchParams = useSearchParams();
+
+  return (
+    <div className="w-full max-w-screen-2xl mx-auto px-4 py-6">
+      <h2 className="text-base font-medium text-black mb-4">Hot Search</h2>
+      <div className="flex flex-wrap items-center justify-start gap-3">
+        {hotTags.map((tag) => {
+          // 构造带q参数的搜索url
+          const params = new URLSearchParams(searchParams.toString());
+          params.set('q', tag);
+          const tagUrl = createUrl('/search', params);
+
+          return (
+            <Link
+              key={tag}
+              href={tagUrl}
+              className="bg-[#F2F2F2FF] rounded-sm px-[15px] py-1.5 flex items-center justify-center text-xs text-black font-normal"
+            >
+              {tag}
+            </Link>
+          );
+        })}
+      </div>
+    </div>
+  );
+}

+ 17 - 13
src/app/(public)/search/page.tsx

@@ -15,6 +15,7 @@ import MobileFilter from "@/components/theme/filters/MobileFilter";
 import FilterList from "@/components/theme/filters/FilterList";
 import { ProductsResponse } from "@/components/catalog/type";
 import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
+import HotSearch from "./_components/HotSearch"
 const Pagination = dynamicImport(
   () => import("@/components/catalog/Pagination"),
 );
@@ -176,15 +177,12 @@ export default async function SearchPage({
   return (
     <>
       <MobileSearchBar />
-      <h2 className="text-2xl sm:text-4xl font-semibold mx-auto mt-7.5 w-full max-w-screen-2xl my-3 mx-auto px-4 xss:px-7.5">
-        All Top Products
-      </h2>
-
-      <div className="my-10 hidden gap-4 md:flex md:items-baseline md:justify-between w-full mx-auto max-w-screen-2xl px-4 xss:px-7.5">
+      <HotSearch />
+      {/* <div className="my-10 hidden gap-4 md:flex md:items-baseline md:justify-between w-full mx-auto max-w-screen-2xl px-4 xss:px-7.5">
         <FilterList filterAttributes={filterAttributes} />
 
         <SortOrder sortOrders={SortByFields} title="Sort by" />
-      </div>
+      </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} />
@@ -193,13 +191,19 @@ export default async function SearchPage({
         </div>
       ) : null}
       {!isArray(products) && (
-        <NotFound
-          msg={`${
-            searchValue
-              ? `There are no products that match Showing : ${searchValue}`
-              : "There are no products that match Showing"
-          } `}
-        />
+        // <NotFound
+        //   msg={`${
+        //     searchValue
+        //       ? `There are no products that match Showing : ${searchValue}`
+        //       : "There are no products that match Showing"
+        //   } `}
+        // />
+        <div>
+          <h2>There are no products that match Showing : ${searchValue}</h2>
+          <h2 className="text-2xl sm:text-4xl font-semibold mx-auto mt-7.5 w-full max-w-screen-2xl my-3 mx-auto px-4 xss:px-7.5">
+            All Top Products
+          </h2>
+        </div>
       )}
       {isArray(products) ? (
         <Grid className="grid grid-flow-row grid-cols-2 gap-5 lg:gap-11.5 w-full max-w-screen-2xl mx-auto md:grid-cols-3 lg:grid-cols-4 px-4 xss:px-7.5">

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

@@ -0,0 +1,59 @@
+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 track_number = searchParams.get("track_number");
+   
+
+    // 👇 构建查询参数数组(自动过滤空值)
+    const queryParts: string[] = [];
+    if (track_number) queryParts.push(`track_number=${track_number}`);
+   
+
+    // 👇 最终拼接 URL
+    let apiUrl = "/customer/orders/tracking"; 
+    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 }
+    );
+  }
+}

+ 1 - 1
src/components/layout/navbar/MobileSearch.tsx

@@ -54,7 +54,7 @@ export const MobileSearchBar = ({
 }) => {
   return (
     <>
-      <div className="block w-full justify-center md:hidden px-4 mt-2 z-10">
+      <div className="block w-full justify-center md:hidden px-4 mt-3 z-10">
         <Suspense fallback={<SearchSkeleton />}>
           <Search search={false} onClose={onClose} />
         </Suspense>

+ 19 - 3
src/components/layout/navbar/Search.tsx

@@ -23,7 +23,22 @@ export default function Search({
   const [searchValue, setSearchValue] = useState(
     searchParams?.get("q") || ""
   );
-
+  // 1. 定义循环展示的提示文案数组
+  const placeholderList = [
+    "Search for products...",
+    "Find best sellers",
+    "Search new arrivals",
+    "Search discount items",
+    "Search trending products"
+  ];
+  const [placeholderIndex, setPlaceholderIndex] = useState(0);
+  // 原始搜索值
+  useEffect(() => {
+    const interval = setInterval(() => {
+      setPlaceholderIndex(prev => (prev + 1) % placeholderList.length);
+    }, 3000); // 每3秒切换一次
+  }, [placeholderList.length]);
+    // 防抖跳转
   useEffect(() => {
     const handler = setTimeout(() => {
       const newParams = new URLSearchParams(searchParams.toString());
@@ -68,7 +83,8 @@ export default function Search({
     }
   };
   const isDesktop = useMediaQuery("(min-width: 1024px)");
-
+   // 当前显示的占位文案
+  const currentPlaceholder = placeholderList[placeholderIndex];
   return (
   <div className={`${isDesktop ? "max-w-[550px]" : ""} relative w-full mx-auto xl:min-w-[516px] outline-none hover:outline-none`}>
       {setSearch && (
@@ -89,7 +105,7 @@ export default function Search({
         autoComplete="off"
         className="input w-full rounded-lg border border-neutral-200 bg-white py-2 pl-3 pr-10 text-sm text-black outline-none placeholder:text-neutral-500 focus:ring-2 focus:ring-neutral-300 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400 md:pl-4"
         name="search"
-        placeholder="Search for products..."
+        placeholder={currentPlaceholder}
         type="text"
       />
 

+ 29 - 16
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";
@@ -50,20 +50,33 @@ export default async function Navbar() {
               <Suspense fallback={<NavigationSkeleton />}>
                 <MobileMenuTrigger menuData={menuData} />
               </Suspense>
-              <Link
-                className="flex-none 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"