zhangzf пре 3 недеља
родитељ
комит
840ed7aac2

Разлика између датотеке није приказан због своје велике величине
+ 141 - 0
src/app/(public)/customer/account/_components/AccountHeader.tsx


+ 242 - 0
src/app/(public)/customer/account/_components/AccountVipPopup.tsx

@@ -0,0 +1,242 @@
+"use client";
+import Link from "next/link";
+// 轮播逻辑
+import { Swiper, SwiperSlide } from "swiper/react";
+import { Navigation } from "swiper/modules";
+import "swiper/css";
+import "swiper/css/navigation";
+
+// 模拟你的每一页数据,方便维护
+const vipList = [
+  {
+    level: 1,
+    title: "V1 Privileges",
+    exppoints: 1000,
+    coupons: [
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130956.png",
+        text1: "Points Redeem",
+        text2: "Giftcard",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130957.png",
+        text1: "Exclusive",
+        text2: "$100 Coupon",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130958.png",
+        text1: "Birthday",
+        text2: "Treat",
+        link: "javascript:void(0)",
+      },
+    ],
+  },
+  {
+    level: 2,
+    title: "V2 Privileges",
+    exppoints: 2000,
+    coupons: [
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130956.png",
+        text1: "POINTS REDEEM",
+        text2: "GIFTCARD",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130957.png",
+        text1: "EXCLUSIVE",
+        text2: "$150 COUPON",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130958.png",
+        text1: "BIRTHDAY",
+        text2: "TREAT",
+        link: "javascript:void(0)",
+      },
+    ],
+  },
+  {
+    level: 3,
+    title: "V3 Privileges",
+    exppoints: 10000,
+    coupons: [
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130956.png",
+        text1: "POINTS REDEEM",
+        text2: "GIFTCARD",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130957.png",
+        text1: "EXCLUSIVE",
+        text2: "$200 COUPON",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130958.png",
+        text1: "BIRTHDAY",
+        text2: "TREAT",
+        link: "javascript:void(0)",
+      },
+    ],
+  },
+  {
+    level: 4,
+    title: "V4 Privileges",
+    exppoints: 50000,
+    coupons: [
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130956.png",
+        text1: "POINTS REDEEM",
+        text2: "GIFTCARD",
+        link: "/customer/account/pointsreceivelist",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130957.png",
+        text1: "EXCLUSIVE",
+        text2: "$300 COUPON",
+        link: "/customer/account/couponsreceivelist/v/4/",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130958.png",
+        text1: "BIRTHDAY",
+        text2: "TREAT",
+        link: "/customer/account/birthday",
+      },
+    ],
+  },
+];
+export default function AccountVipPopup({
+  visible,
+  onClose,
+}: {
+  visible: boolean;
+  onClose: () => void;
+}) {
+  // 如果不显示,直接 return null
+  if (!visible) return null;
+
+  return (
+    // 全屏遮罩
+    <div className="fixed inset-0 z-50 bg-black/50">
+      {/* 弹框主体 - 从底部滑上来 */}
+      <div className="absolute bottom-0 rounded-none left-0 right-0 w-full h-full bg-white  overflow-auto">
+        <div className="flex flex-col">
+          {/* 顶部导航 */}
+          <header className="bg-black text-white py-4 px-4 flex items-center">
+            <button onClick={onClose} className="">
+              <svg
+                xmlns="http://www.w3.org/2000/svg"
+                className="h-6 w-6"
+                fill="none"
+                viewBox="0 0 24 24"
+                stroke="currentColor"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={2}
+                  d="M15 19l-7-7 7-7"
+                />
+              </svg>
+            </button>
+            <h1 className="mx-auto">Vip Center</h1>
+          </header>
+          <div className="vipcenter-privileges-modal-innerscroll-father">
+            <div id="vipcenter-privileges-modal-innerscroll">
+              <div
+                className="   relative
+    h-170
+    mt-8
+    px-6
+    bg-[url('https://cdn.alipearlhair.com/media/wysiwyg/wap/_2.png')]
+    bg-no-repeat
+    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>
+                </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 "
+                      alt=""
+                    />
+                  </div>
+                  <p className="text-4.5 mt-3 font-semibold"></p>
+                  <p className="text-text-sm mt-1.5 text-[#EFEFEF]">
+                    18567768101@163.com
+                  </p>
+                  <p className="text-sm mt-1.5 text-[#EFEFEF]">
+                    Above current level
+                  </p>
+                </div>
+                <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
+                    </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>
+                  {/* 轮播 */}
+                  <div className="swiper-container-center vip-swiper">
+                    <Swiper
+                      modules={[Navigation]}
+                      navigation // 开启左右箭头
+                      slidesPerView={1}
+                      spaceBetween={0}
+                      className="swiper-container-horizontal"
+                    >
+                      {vipList.map((item, idx) => (
+                        <SwiperSlide
+                          key={idx}
+                          className="swiper-slide-color"
+                          style={{ width: "343px" }}
+                          data-exppoints={item.exppoints}
+                          data-level={item.level}
+                        >
+                          {/* 标题 */}
+                          <h3 className="text-lg font-medium mb-4">
+                            {item.title}
+                          </h3>
+
+                          {/* 权益卡片容器 */}
+                          <div className="vipcenter-privileges-coupnts flex gap-3 flex-wrap">
+                            {item.coupons.map((coupon, cIdx) => (
+                              <a
+                                key={cIdx}
+                                href={coupon.link}
+                                className="vipcenter-privileges-coupnt-a vipcenter-privileges-coupnt-ponit"
+                              >
+                                <button className="vipcenter-privileges-coupnt flex flex-col items-center p-2 border rounded">
+                                  <img
+                                    src={coupon.img}
+                                    alt=""
+                                    className="w-10 h-10 object-contain mb-1"
+                                  />
+                                  <p className="text-sm">{coupon.text1}</p>
+                                  <p className="text-sm">{coupon.text2}</p>
+                                </button>
+                              </a>
+                            ))}
+                          </div>
+                        </SwiperSlide>
+                      ))}
+                    </Swiper>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 1 - 1
src/app/(public)/customer/account/mypoints/page.tsx

@@ -74,7 +74,7 @@ const AccountPointsPage = () => {
             ></path>
           </svg>
           </Link>
-        <span className="vipReturnTitles">SHIPPING ADDRESS</span>
+        <span className="vipReturnTitles">My Points</span>
       </div>
       <div className=" bg-[#FFF5F2] px-4 py-8 max-w-3xl mx-auto pt-6 pb-5">
         {/* 顶部积分展示区 */}

Разлика између датотеке није приказан због своје велике величине
+ 4 - 97
src/app/(public)/customer/account/page.tsx


+ 58 - 0
src/app/(public)/customer/address/_components/DefaultAddress.tsx

@@ -0,0 +1,58 @@
+"use client";
+import { useState, useEffect } from "react";
+import { clientFetch } from "@/lib/restApiClient";
+import Link from "next/link";
+export default function DefaultAddress() {
+  const [addressDatas, setAddressDatas] = useState<any[]>([]); // 加 any[] 提示
+
+  useEffect(() => {
+    const load = async () => {
+      const page = 1;
+      const resp = await clientFetch(
+        `/api/customer/token/address`, ///api/shop/customer-addresses?page=${page}
+      );
+      console.log("完整返回:", resp.data);
+      resp.data.data= Array.isArray(resp.data.data) ? resp.data.data : [];
+      const sortedList = [...resp.data.data].sort((a, b) => {
+        // true(1) 排在 false(0) 前面
+        if (a.default_address !== b.default_address) {
+          return b.default_address ? 1 : -1;
+        }
+        // 相同状态,保留原顺序(返回0即可)
+        return 0;
+      });
+      setAddressDatas(sortedList);
+      console.log("aaaa",sortedList,addressDatas);
+      
+    };
+    load();
+  }, []);
+  return (
+    <>
+      {(addressDatas || []).map((item) => (
+        <div key={item.id} className="mb-6">
+          {item.default_address ? (
+            <h2 className="text-xl font-bold uppercase mb-3">
+              Default Billing Address
+            </h2>
+          ) : null}
+          <div className="border border-gray-200 p-4">
+            <p className="mb-1">{item.first_name + item.last_name}</p>
+            <p className="mb-1">{item.address}</p>
+            <p className="mb-1">{item.city + "," + item.postcode}</p>
+            <p className="mb-1">{item.country}</p>
+            <p className="mb-4">T: {item.phone}</p>
+            <div className="text-right">
+              <Link
+                href={`/customer/address/eidt/${item.id}`}
+                className="text-blue-600 underline hover:text-blue-800"
+              >
+                Change Billing Address
+              </Link>
+            </div>
+          </div>
+        </div>
+      ))}
+    </>
+  );
+}

+ 304 - 0
src/app/(public)/customer/address/eidt/[id]/page.tsx

@@ -0,0 +1,304 @@
+"use client";
+import { useState, useEffect, useMemo } from "react";
+import { useParams } from "next/navigation";
+import Link from "next/link";
+import { clientFetch } from "@/lib/restApiClient";
+import Select from "@/components/theme/ui/Select";
+import { useForm, FormProvider, Controller } from "react-hook-form";
+import CustomerAddressPhone from "../../new/_components/CustomerAddressPhone";
+const CustomerAddressEditPage = () => {
+  // 定义表单类型(严格对齐你要的字段)
+  interface AddressFormData {
+    first_name: string;
+    last_name: string;
+    email: string;
+    phone: string;
+    address: string[];
+    country: string;
+    state: string;
+    city: string;
+    postcode: string;
+    default_address: number;
+  }
+  const { id } = useParams();
+  // const [form, setForm] = useState({});
+  // 1. 根据ID获取地址详情
+  console.log("addressid:", id);
+  const COUNTRIES = [
+    { id: 1, code: "AF", name: "Afghanistan" },
+    { id: 2, code: "AX", name: "Åland Islands" },
+    { id: 3, code: "AL", name: "Albania" },
+    { id: 4, code: "DZ", name: "Algeria" },
+    { id: 49, code: "CN", name: "China" },
+    { id: 244, code: "US", name: "United States" },
+  ];
+  const countriesOptions = useMemo(() => {
+    return COUNTRIES.map((country) => ({
+      value: country.code,
+      label: country.name,
+      id: String(country.id),
+    }));
+  }, [COUNTRIES]);
+  const [actionType, setActionType] = useState<"save" | "delete">("save");
+    // 修改地址到接口
+    const UpdataAddress = async (formData: AddressFormData) => {
+      const res = await clientFetch(`/api/customer/token/address?id=${id}`, {
+        method: "PUT",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(formData),
+      });
+      const json = await res;
+      console.log("提交成功,返回结果:", json);
+      if(json?.data?.success){
+          window.location.reload();
+      }else{
+        alert("error");
+      }
+      return json;
+    };
+  // 删除地址接口
+  const  DeleteAddress =async ()=>{
+     const res = await clientFetch(`/api/customer/token/address?id=${id}`, {
+        method: "DELETE",
+        headers: {
+          "Content-Type": "application/json",
+        }
+      });
+      const json = await res;
+      console.log("删除成功,返回结果:", json);
+  }
+  // 表单提交
+  const addressFormOnSubmit = async (formData: AddressFormData) => {
+    console.log("actionType",actionType);
+    
+    if (actionType === "save") {
+      UpdataAddress(formData);
+    } else {
+      DeleteAddress();
+      console.log("执行删除");
+    }
+  };
+
+  // 表单实例(严格对齐你需要的字段)
+  const addressForm = useForm<AddressFormData>({
+    mode: "onBlur",
+    reValidateMode: "onChange",
+    defaultValues: {
+      first_name: "",
+      last_name: "",
+      email: "",
+      phone: "",
+      address: ["", ""], // 数组格式
+      country: "",
+      state: "",
+      city: "",
+      postcode: "",
+      default_address: 0,
+    },
+  });
+
+  const {
+    reset,
+    register,
+    handleSubmit,
+    formState: { errors, isValid },
+  } = addressForm;
+  useEffect(() => {
+    if (!id) return;
+    const load = async () => {
+      const resp = await clientFetch(`/api/customer/token/address?id=${id}`);
+      console.log("【完整接口响应】", resp);
+
+      // 自动兼容后端 99% 的返回格式
+      const formData = resp.data?.data || resp.data || {};
+      console.log("✅ 回填数据:", formData);
+
+      reset(formData);
+    };
+    load();
+  }, [id, reset]);
+  return (
+    <div className="w-full h-full">
+      <div  className="bg-[#fff] pr-2.5 pl-2.5 w-full text-center text-base h-11 leading-11 font-semibold relative text-[#0b0b0b]">
+        <Link
+          href={"/customer/address"}
+          // onClick={() => window.history.back()}
+          className="absolute left-2.5 text-2xl inline-block top-1/2  -translate-y-1/2"
+        >
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            width="24"
+            height="24"
+            viewBox="0 0 24 24"
+            fill="none"
+          >
+            <path
+              stroke="rgba(0, 0, 0, 1)"
+              stroke-width="1.2"
+              stroke-linejoin="round"
+              stroke-linecap="round"
+              d="M15 18L9 12L15 6"
+            ></path>
+          </svg>
+        </Link>
+        <span className="vipReturnTitles">ADDRESS EDIT</span>
+      </div>
+      <div className="max-w-md mx-auto bg-[#f8f8f8]">
+        <FormProvider {...addressForm}>
+          <form
+            className="overflow-hidden"
+            onSubmit={handleSubmit(addressFormOnSubmit)}
+          >
+            <div className="bg-[#f8f8f8] p-[20px_10px]">
+              {/* 姓名 */}
+              <div className="text-start">*Name</div>
+              <div className="flex justify-between items-center gap-3">
+                {/* firstname */}
+                <input
+                  type="text"
+                  placeholder="firstname"
+                  {...register("first_name", {
+                    required: "First name is required",
+                  })}
+                  className="w-43 h-11 rounded-sm border border-solid border-[rgba(102,102,102,1)] indent-4 text-[#a6a6a6] text-base leading-11 font-normal"
+                />
+                {/* lastname */}
+                <input
+                  type="text"
+                  placeholder="last_name"
+                  {...register("last_name", {
+                    required: "Last name is required",
+                  })}
+                  className="w-43 h-11 rounded-sm border border-solid border-[rgba(102,102,102,1)] indent-4 text-[#a6a6a6] text-base leading-11 font-normal box-[unset]"
+                />
+              </div>
+              {errors.first_name && (
+                <p className="text-red-500 text-sm mt-1">
+                  {errors.first_name.message}
+                </p>
+              )}
+              {errors.last_name && (
+                <p className="text-red-500 text-sm mt-1">
+                  {errors.last_name.message}
+                </p>
+              )}
+
+              {/* 手机号 */}
+              <CustomerAddressPhone />
+              <Controller
+                control={addressForm.control}
+                name="country"
+                rules={{ required: "Country is required" }}
+                render={({ field }) => {
+                  return (
+                    <Select
+                      placeholder="Country/Region"
+                      defaultValue={field.value}
+                      onChange={field.onChange}
+                      onBlur={field.onBlur}
+                      name={field.name}
+                      options={countriesOptions}
+                      error={errors.country?.message as string}
+                    />
+                  );
+                }}
+              />
+              {/* <input {...register("country")} /> */}
+              {/* 邮箱 */}
+              <div className="mt-4">
+                <label>Email Address*</label>
+                <input
+                  type="email"
+                  placeholder="Email"
+                  {...register("email", {
+                    required: "Email is required",
+                    pattern: {
+                      value: /^\S+@\S+\.\S+$/,
+                      message: "Invalid email",
+                    },
+                  })}
+                  className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+                />
+                {errors.email && (
+                  <p className="text-red-500 text-sm mt-1">
+                    {errors.email.message}
+                  </p>
+                )}
+              </div>
+
+              {/* 邮编 */}
+              <div className="mt-4">
+                <label>Zip/Postal Code</label>
+                <input
+                  type="text"
+                  placeholder="Postcode"
+                  {...register("postcode")}
+                  className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+                />
+              </div>
+
+              {/* 省/州 */}
+              <div className="mt-4">
+                <label>State / Province</label>
+                <input
+                  type="text"
+                  placeholder="State"
+                  {...register("state")}
+                  className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+                />
+              </div>
+
+              {/* 城市 */}
+              <div className="mt-4">
+                <label>City</label>
+                <input
+                  type="text"
+                  placeholder="City"
+                  {...register("city")}
+                  className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+                />
+              </div>
+
+              {/* 默认地址 */}
+              <label className="flex items-center gap-2 cursor-pointer mt-4">
+                <input
+                  type="checkbox"
+                  {...register("default_address", { valueAsNumber: true })}
+                  className="w-5 h-5"
+                />
+                <span>Use as my default address</span>
+              </label>
+
+              {/* 提交按钮 */}
+              <button
+                className="w-full h-11 leading-11 bg-[#1bbc9b] text-white text-center mt-6 rounded"
+                type="button"
+                onClick={() => {
+                  setActionType("save");
+                  document.getElementById("realSubmit")?.click(); 
+                }}
+              >
+                Save Address
+              </button>
+              <button
+                className="w-full h-11 leading-11 bg-[#1bbc9b] text-white text-center mt-6 rounded"
+                type="button"
+                onClick={() => {
+                  if (!confirm("确定删除该地址?")) return;
+                  setActionType("delete");
+                  document.getElementById("realSubmit")?.click(); 
+                }}
+              >
+                Delete Address
+              </button>
+              <button id="realSubmit" type="submit" hidden />
+            </div>
+          </form>
+        </FormProvider>
+      </div>
+    </div>
+  );
+};
+export default CustomerAddressEditPage;

+ 99 - 0
src/app/(public)/customer/address/new/_components/CustomerAddressPhone.tsx

@@ -0,0 +1,99 @@
+"use client";
+import {
+  formatFinalPhoneValue,
+  getPhoneCodeBycountryCode,
+} from "@/components/theme/ui/PhoneNumberInput/phoneCodeMetaData";
+
+import { useMemo, useEffect, useState } from "react";
+import { useQuery } from "@apollo/client/react";
+import { useFormContext, Controller, useWatch } from "react-hook-form";
+import { IS_VALID_FULL_PHONE, IS_VALID_PHONECODE } from "@utils/constants";
+import { GET_COUNTRY_STATES } from "@/graphql";
+import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
+
+export default function CustomerAddressPhone() {
+  const {
+    getValues,
+    setValue,
+    formState: { errors },
+    control,
+  } = useFormContext();
+
+  // 🔴 修复1:监听父表单的 country 字段(原 shippingCountry 是错误字段)
+  const selectedCountryCode = useWatch({
+    control,
+    name: "country",
+  });
+
+  // 是否用户手动选择过区号
+  const [hasUserSelectedPhoneCode, setHasUserSelectedPhoneCode] =
+    useState(false);
+
+  // 国家切换 → 自动同步电话区号(兼容 reset 回填)
+  useEffect(() => {
+    const currentPhone = getValues("phone");
+    const regPhoneCode = IS_VALID_PHONECODE;
+
+    if (currentPhone && !hasUserSelectedPhoneCode) {
+      const newPhoneCode = getPhoneCodeBycountryCode(selectedCountryCode);
+      const codeMatch = regPhoneCode.exec(currentPhone);
+      let purePhone = currentPhone;
+
+      // 剥离已有区号
+      if (codeMatch) {
+        purePhone = currentPhone.replace(codeMatch[0], "");
+      }
+
+      // 新区号和原有区号不一致才更新
+      if (codeMatch && newPhoneCode !== codeMatch[0].trim()) {
+        setValue("phone", formatFinalPhoneValue(newPhoneCode, purePhone));
+      }
+    }
+  }, [selectedCountryCode, hasUserSelectedPhoneCode, getValues, setValue]);
+
+  const { data: statesData, loading: statesLoading } = useQuery(
+    GET_COUNTRY_STATES,
+    {
+      variables: {},
+      skip: !selectedCountryCode,
+    },
+  );
+
+  return (
+    <div className="w-full mt-4">
+      <label className="text-ly-12 block mb-4 font-semibold">
+        Phone number *
+      </label>
+      {/* ✅ Controller 本身写法没问题,保留即可,字段 name="phone" 和父表单一致 */}
+      <Controller
+        name="phone"
+        control={control}
+        rules={{
+          required: "Phone Number is required",
+          pattern: {
+            value: IS_VALID_FULL_PHONE,
+            message: "Invalid Phone Number",
+          },
+        }}
+        render={({ field, fieldState }) => {
+          return (
+            <PhoneNumberInput
+              placeholder="Please Input Your Phone Number"
+              value={field.value}
+              onChange={field.onChange}
+              onBlur={field.onBlur}
+              name={field.name}
+              ref={field.ref}
+              error={fieldState.error?.message}
+              countryCode={selectedCountryCode}
+              hasUserSelectedPhoneCode={hasUserSelectedPhoneCode}
+              onHasUserSelectedPhoneCode={() =>
+                setHasUserSelectedPhoneCode(true)
+              }
+            />
+          );
+        }}
+      />
+    </div>
+  );
+}

+ 222 - 0
src/app/(public)/customer/address/new/_components/NewAddressDefault.tsx

@@ -0,0 +1,222 @@
+"use client";
+import { useForm, FormProvider } from "react-hook-form";
+import { useMemo } from "react";
+import CustomerAddressPhone from "./CustomerAddressPhone";
+import Select from "@/components/theme/ui/Select";
+import { clientFetch } from "@/lib/restApiClient";
+
+// 定义表单类型(严格对齐你要的字段)
+interface AddressFormData {
+  first_name: string;
+  last_name: string;
+  email: string;
+  phone: string;
+  address: string[];
+  country: string;
+  state: string;
+  city: string;
+  postcode: string;
+  default_address: number;
+}
+
+export default function NewAddressDefault({ countries }: { countries: any[] }) {
+  // 国家选项
+  const countriesOptions = useMemo(() => {
+    return countries.map((country) => ({
+      value: country.code,
+      label: country.name,
+      id: String(country.id),
+    }));
+  }, [countries]);
+
+  // 提交地址到接口
+  const AddAdress = async (formData: AddressFormData) => {
+    const res = await clientFetch("/api/customer/token/address", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(formData),
+    });
+    const json = await res;
+    console.log("提交成功,返回结果:", json);
+    if(json?.status== 201){
+        window.location.href = "/customer/address/";
+    }else{
+      alert("error");
+    }
+    return json;
+  };
+
+  // 表单提交
+  const addressFormOnSubmit = async (formData: AddressFormData) => {
+    console.log("最终提交给后端的数据:", formData);
+    await AddAdress(formData);
+  };
+
+  // 表单实例(严格对齐你需要的字段)
+  const addressForm = useForm<AddressFormData>({
+    mode: "onBlur",
+    reValidateMode: "onChange",
+    defaultValues: {
+      first_name: "",
+      last_name: "",
+      email: "",
+      phone: "",
+      address: ["", ""], // 数组格式
+      country: "",
+      state: "",
+      city: "",
+      postcode: "",
+      default_address: 0,
+    },
+  });
+
+  const {
+    register,
+    handleSubmit,
+    formState: { errors, isValid },
+  } = addressForm;
+
+  return (
+    <div className="w-full h-full">
+      <FormProvider {...addressForm}>
+        <form
+          className="overflow-hidden"
+          onSubmit={handleSubmit(addressFormOnSubmit)}
+        >
+          {/* 头部 */}
+          <div className="bg-[#fff] px-2.5 w-full text-center text-base h-11 leading-11 font-semibold relative text-[#0b0b0b]">
+            <div
+              onClick={() => window.history.back()}
+              className="absolute left-2.5 text-2xl inline-block top-1/2 -translate-y-1/2"
+            >
+              <svg
+                xmlns="http://www.w3.org/2000/svg"
+                width="24"
+                height="24"
+                viewBox="0 0 24 24"
+                fill="none"
+              >
+                <path
+                  stroke="rgba(0, 0, 0, 1)"
+                  strokeWidth="1.2"
+                  strokeLinejoin="round"
+                  strokeLinecap="round"
+                  d="M15 18L9 12L15 6"
+                ></path>
+              </svg>
+            </div>
+            <span>Add New Address</span>
+          </div>
+
+          <div className="bg-[#f8f8f8] p-[20px_10px]">
+            {/* 姓名 */}
+             <div className="text-start">*Name</div>
+            <div className="flex justify-between items-center gap-3">
+              {/* firstname */}
+              <input
+                type="text"
+                placeholder="firstname"
+                {...register("first_name", {
+                  required: "First name is required",
+                })}
+                className="w-43 h-11 rounded-sm border border-solid border-[rgba(102,102,102,1)] indent-4 text-[#a6a6a6] text-base leading-11 font-normal"
+              />
+              {/* lastname */}
+              <input
+                type="text"
+                placeholder="last_name"
+                {...register("last_name", {
+                  required: "Last name is required",
+                })}
+                className="w-43 h-11 rounded-sm border border-solid border-[rgba(102,102,102,1)] indent-4 text-[#a6a6a6] text-base leading-11 font-normal box-[unset]"
+              />
+            </div>
+            {errors.first_name && <p className="text-red-500 text-sm mt-1">{errors.first_name.message}</p>}
+            {errors.last_name && <p className="text-red-500 text-sm mt-1">{errors.last_name.message}</p>}
+
+            {/* 手机号 */}
+            <CustomerAddressPhone />
+
+            {/* 国家 */}
+            <div className="mt-4">
+              <Select
+                placeholder="Country/Region"
+                {...register("country", { required: "Country is required" })}
+                options={countriesOptions}
+                error={errors.country?.message as string}
+              />
+            </div>
+
+            {/* 邮箱 */}
+            <div className="mt-4">
+              <label>Email Address*</label>
+              <input
+                type="email"
+                placeholder="Email"
+                {...register("email", {
+                  required: "Email is required",
+                  pattern: { value: /^\S+@\S+\.\S+$/, message: "Invalid email" },
+                })}
+                className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+              />
+              {errors.email && <p className="text-red-500 text-sm mt-1">{errors.email.message}</p>}
+            </div>
+
+            {/* 邮编 */}
+            <div className="mt-4">
+              <label>Zip/Postal Code</label>
+              <input
+                type="text"
+                placeholder="Postcode"
+                {...register("postcode")}
+                className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+              />
+            </div>
+
+            {/* 省/州 */}
+            <div className="mt-4">
+              <label>State / Province</label>
+              <input
+                type="text"
+                placeholder="State"
+                {...register("state")}
+                className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+              />
+            </div>
+
+            {/* 城市 */}
+            <div className="mt-4">
+              <label>City</label>
+              <input
+                type="text"
+                placeholder="City"
+                {...register("city")}
+                className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+              />
+            </div>
+
+            {/* 默认地址 */}
+            <label className="flex items-center gap-2 cursor-pointer mt-4">
+              <input
+                type="checkbox"
+                {...register("default_address", { valueAsNumber: true })}
+                className="w-5 h-5"
+              />
+              <span>Use as my default address</span>
+            </label>
+
+            {/* 提交按钮 */}
+            <button
+              className="w-full h-11 leading-11 bg-[#1bbc9b] text-white text-center mt-6 rounded"
+              type="submit"
+            >
+              Save Address
+            </button>
+          </div>
+        </form>
+      </FormProvider>
+    </div>
+  );
+}

Разлика између датотеке није приказан због своје велике величине
+ 1286 - 169
src/app/(public)/customer/address/new/page.tsx


+ 9 - 8
src/app/(public)/customer/address/page.tsx

@@ -1,8 +1,7 @@
-"use client";
 import React from "react";
-// import { useState } from "react";
-// import { useEffect } from "react";
-const CustomerAddressPage = () => {
+import Link from "next/link";
+import  DefaultAddress from "./_components/DefaultAddress";
+const CustomerAddressPage = async () => {
   // 模拟地址数据(可根据实际需求替换)
   const addressData = {
     billing: {
@@ -22,11 +21,12 @@ const CustomerAddressPage = () => {
       phone: "2123165641",
     },
   };
+ 
   return (
     <div className="w-full h-full">
       <div className="bg-[#fff] pr-2.5 pl-2.5 w-full text-center text-base h-11 leading-11 font-semibold relative text-[#0b0b0b]">
-        <div
-          onClick={() => window.history.back()}
+        <Link href={""}
+          // onClick={() => window.history.back()}
           className="absolute left-2.5 text-2xl inline-block top-1/2  -translate-y-1/2"
         >
           <svg
@@ -44,10 +44,11 @@ const CustomerAddressPage = () => {
               d="M15 18L9 12L15 6"
             ></path>
           </svg>
-        </div>
+        </Link>
         <span className="vipReturnTitles">SHIPPING ADDRESS</span>
       </div>
       <div className="max-w-md mx-auto p-4 bg-[#f8f8f8]">
+        <DefaultAddress  />
         {/* 账单地址模块 */}
         <div className="mb-6">
           <h2 className="text-xl font-bold uppercase mb-3">
@@ -70,7 +71,7 @@ const CustomerAddressPage = () => {
             </div>
           </div>
         </div>
-
+        
         {/* 收货地址模块 */}
         <div>
           <h2 className="text-xl font-bold uppercase mb-3">

+ 193 - 0
src/app/api/customer/token/address/route.ts

@@ -0,0 +1,193 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+// import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
+export async function GET(req: NextRequest, { params }: { params: { id?: string } }) {
+  try {
+    const guestToken = getAuthToken(req);
+    // 从url取查询参数 id
+    const addressId = req.nextUrl.searchParams.get("id");
+
+    const apiUrl = addressId
+      ? `/customer/token/address/${addressId}`
+      : `/customer/token/address`;
+
+    console.log("11111----:", 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 },
+    );
+  }
+}
+export async function POST(req: NextRequest) {
+  try {
+    const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+    const params = await req.json();
+    const response = await restApiFetch<{
+      data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
+      variables: {
+        first_name: string;
+        last_name: string;
+        email: string;
+        phone: string;
+        address: string;
+        country: string;
+        state: string;
+        city: string;
+        postcode: string;
+        default_address: number;
+      };
+    }>({
+      api: "/customer/token/address",
+      method: "POST",
+      cache: "no-store",
+      variables: params,
+      guestToken: authorizationToken,
+    });
+    // 打印后端原始返回结构
+    console.log(
+      "接口原始response.body =",
+      JSON.stringify(response.body, null, 2),
+    );
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+  } catch (error) {
+    console.log("/customer/token/address---", 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 },
+    );
+  }
+}
+export async function PUT(req: NextRequest) {
+  try {
+    const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+    const params = await req.json();
+    // 从url取查询参数 id
+    const addressId = req.nextUrl.searchParams.get("id");
+    const response = await restApiFetch<{
+      data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
+      variables: {
+        first_name: string;
+        last_name: string;
+        email: string;
+        phone: string;
+        address: string;
+        country: string;
+        state: string;
+        city: string;
+        postcode: string;
+        default_address: number;
+      };
+    }>({
+      api: `/customer/token/address/${addressId}`,
+      method: "PUT",
+      cache: "no-store",
+      variables: params,
+      guestToken: authorizationToken,
+    });
+    // 打印后端原始返回结构
+    console.log(
+      "接口原始response.body =",
+      JSON.stringify(response.body, null, 2),
+    );
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+  } catch (error) {
+    console.log("/customer/token/address---", 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 },
+    );
+  }
+}
+export async function DELETE(req: NextRequest, { params }: { params: { id?: string } }) {
+  try {
+    const guestToken = getAuthToken(req);
+    const addressId = req.nextUrl.searchParams.get("id");
+    const response = await restApiFetch<any>({
+      api: `/customer/token/address/${addressId}`, //
+      method: "DELETE",
+      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 },
+    );
+  }
+}

+ 40 - 0
src/app/api/shop/customer-address-gets/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";
+import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
+export async function GET(req: NextRequest,{ params}:{ params: Promise<{ id: string }>}) {
+    try {
+        const guestToken = getAuthToken(req);
+        // 从查询参数取 page,默认 1
+        const id = req.nextUrl.searchParams.get("id") || "1";
+        const response = await restApiFetch<any>({
+            api: `/shop/customer-address-gets?id=${id}`,
+            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 }
+        );
+    }
+}

+ 40 - 0
src/app/api/shop/customer-addresses/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";
+import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
+export async function GET(req: NextRequest,{ params}:{ params: Promise<{ page: string }>}) {
+    try {
+        const guestToken = getAuthToken(req);
+        // 从查询参数取 page,默认 1
+        const page = req.nextUrl.searchParams.get("page") || "1";
+        const response = await restApiFetch<any>({
+            api: `/shop/customer-addresses?page=${page}`,
+            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/types/checkout/type.ts

@@ -295,7 +295,7 @@ export interface CreateCheckoutAddressOperation {
 export interface Country {
   code: string;
   name: string;
-  _id: number;
+  // _id: number;
   id: string;
 }
 

+ 3 - 3
src/utils/bagisto/index.ts

@@ -59,7 +59,7 @@ export async function restApiFetch<T>({
   revalidate = 60,
 }: {
   api: string;
-  method: "POST" | "GET";
+  method: "POST" | "GET" | "PUT" | "DELETE";
   cache?: RequestCache;
   headers?: HeadersInit | Record<string, string>;
   tags?: string[];
@@ -104,7 +104,7 @@ export async function restApiFetch<T>({
         Object.assign(baseHeaders, headers);
       }
     }
-console.log('restApiFetch --- baseHeaders:', baseHeaders)
+
     const param: RequestInit = {
       method: method,
       headers: baseHeaders,
@@ -117,7 +117,7 @@ console.log('restApiFetch --- baseHeaders:', baseHeaders)
     if(variables) {
       param.body = JSON.stringify({...variables});
     }
-
+console.log('restApiFetch --- baseHeaders:', baseHeaders,apiUrl, param)
     const result = await fetch(apiUrl, param);
     console.log('restApiFetch --- result:', result)
     const body = await result.json();