|
|
@@ -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>
|
|
|
</>
|