|
@@ -0,0 +1,308 @@
|
|
|
|
|
+"use client";
|
|
|
|
|
+// import Image from "next/image";
|
|
|
|
|
+import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
|
|
|
+
|
|
|
|
|
+// 模拟积分明细数据(可替换为接口请求)
|
|
|
|
|
+const mockPointsData = [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 1,
|
|
|
|
|
+ type: "rewarded",
|
|
|
|
|
+ title: "Check In",
|
|
|
|
|
+ points: "+10 Points",
|
|
|
|
|
+ date: "May 17, 2026 11:07 PM",
|
|
|
|
|
+ currentPoints: "My points:100009",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 2,
|
|
|
|
|
+ type: "rewarded",
|
|
|
|
|
+ title: "Points Return of Redeem Hair Usage",
|
|
|
|
|
+ points: "+12000 Points",
|
|
|
|
|
+ date: "Oct 23, 2025 1:11 AM",
|
|
|
|
|
+ currentPoints: "My points:99999",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 3,
|
|
|
|
|
+ type: "used",
|
|
|
|
|
+ title: "Points Redeem Hair",
|
|
|
|
|
+ points: "-6000 Points",
|
|
|
|
|
+ date: "Oct 17, 2025 10:27 PM",
|
|
|
|
|
+ currentPoints: "My points:87999",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 4,
|
|
|
|
|
+ type: "used",
|
|
|
|
|
+ title: "Points Redeem Hair",
|
|
|
|
|
+ points: "-6000 Points",
|
|
|
|
|
+ date: "Oct 17, 2025 10:25 PM",
|
|
|
|
|
+ currentPoints: "My points:93999",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 5,
|
|
|
|
|
+ type: "rewarded",
|
|
|
|
|
+ title: "Updated by Admin",
|
|
|
|
|
+ points: "+99999 Points",
|
|
|
|
|
+ date: "Oct 17, 2025 10:24 PM",
|
|
|
|
|
+ currentPoints: "My points:99999",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 6,
|
|
|
|
|
+ type: "used",
|
|
|
|
|
+ title: "Points Redeem Giftcard",
|
|
|
|
|
+ points: "-300 Points",
|
|
|
|
|
+ date: "Oct 13, 2025 8:41 PM",
|
|
|
|
|
+ currentPoints: "My points:0",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 7,
|
|
|
|
|
+ type: "rewarded",
|
|
|
|
|
+ title: "Reward for Registering",
|
|
|
|
|
+ points: "+200 Points",
|
|
|
|
|
+ date: "Jun 9, 2025 7:39 PM",
|
|
|
|
|
+ currentPoints: "My points:300",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 8,
|
|
|
|
|
+ type: "rewarded",
|
|
|
|
|
+ title: "Reward for Signing up Newsletter",
|
|
|
|
|
+ points: "+100 Points",
|
|
|
|
|
+ date: "Jun 9, 2025 7:39 PM",
|
|
|
|
|
+ currentPoints: "My points:100",
|
|
|
|
|
+ },
|
|
|
|
|
+ // 补充测试数据(凑够分页演示)
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 9,
|
|
|
|
|
+ type: "rewarded",
|
|
|
|
|
+ title: "Check In",
|
|
|
|
|
+ points: "+10 Points",
|
|
|
|
|
+ date: "May 16, 2026 10:00 PM",
|
|
|
|
|
+ currentPoints: "My points:100000",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 10,
|
|
|
|
|
+ type: "used",
|
|
|
|
|
+ title: "Points Redeem Accessory",
|
|
|
|
|
+ points: "-500 Points",
|
|
|
|
|
+ date: "May 15, 2026 9:00 AM",
|
|
|
|
|
+ currentPoints: "My points:99990",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 11,
|
|
|
|
|
+ type: "rewarded",
|
|
|
|
|
+ title: "Review Product",
|
|
|
|
|
+ points: "+50 Points",
|
|
|
|
|
+ date: "May 14, 2026 8:00 PM",
|
|
|
|
|
+ currentPoints: "My points:100490",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 12,
|
|
|
|
|
+ type: "rewarded",
|
|
|
|
|
+ title: "Refer a Friend",
|
|
|
|
|
+ points: "+200 Points",
|
|
|
|
|
+ date: "May 13, 2026 7:00 PM",
|
|
|
|
|
+ currentPoints: "My points:100440",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 13,
|
|
|
|
|
+ type: "used",
|
|
|
|
|
+ title: "Points Redeem Cash",
|
|
|
|
|
+ points: "-1000 Points",
|
|
|
|
|
+ date: "May 12, 2026 6:00 PM",
|
|
|
|
|
+ currentPoints: "My points:100240",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 14,
|
|
|
|
|
+ type: "rewarded",
|
|
|
|
|
+ title: "Check In",
|
|
|
|
|
+ points: "+10 Points",
|
|
|
|
|
+ date: "May 11, 2026 5:00 PM",
|
|
|
|
|
+ currentPoints: "My points:101240",
|
|
|
|
|
+ },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const PointsDetails = () => {
|
|
|
|
|
+ // 状态管理
|
|
|
|
|
+ const [activeTab, setActiveTab] = useState("all"); // 激活标签:all/rewarded/used
|
|
|
|
|
+ const [listData, setListData] = useState([]); // 展示的列表数据
|
|
|
|
|
+ const [page, setPage] = useState(1); // 当前页码
|
|
|
|
|
+ const [loading, setLoading] = useState(false); // 加载状态
|
|
|
|
|
+ const [hasMore, setHasMore] = useState(true); // 是否有更多数据
|
|
|
|
|
+ const listRef = useRef(null); // 列表容器ref,用于监听滚动
|
|
|
|
|
+
|
|
|
|
|
+ // 每页展示12条
|
|
|
|
|
+ const PAGE_SIZE = 12;
|
|
|
|
|
+ const fetchData = useCallback((page: number, tab: string) => {
|
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ let data = mockPointsData;
|
|
|
|
|
+ const prePage = page - 1;
|
|
|
|
|
+ const start = prePage * PAGE_SIZE;
|
|
|
|
|
+ const end = page * PAGE_SIZE;
|
|
|
|
|
+ if (tab === "rewarded") {
|
|
|
|
|
+ data = mockPointsData.filter(
|
|
|
|
|
+ (item) => item.type === "rewarded",
|
|
|
|
|
+ );
|
|
|
|
|
+ } else if (tab === "used") {
|
|
|
|
|
+ data = mockPointsData.filter((item) => item.type === "used");
|
|
|
|
|
+ }
|
|
|
|
|
+ const res = data.slice(start, end);
|
|
|
|
|
+ resolve(res);
|
|
|
|
|
+ }, 800);
|
|
|
|
|
+ });
|
|
|
|
|
+ },[]);
|
|
|
|
|
+ // 加载更多数据
|
|
|
|
|
+ const loadMore = useCallback(() => {
|
|
|
|
|
+
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+
|
|
|
|
|
+ fetchData(page+1,activeTab).then((res) => {
|
|
|
|
|
+
|
|
|
|
|
+ setListData(res);
|
|
|
|
|
+
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ setPage(page+1);
|
|
|
|
|
+ if(res.length < PAGE_SIZE) {
|
|
|
|
|
+ setHasMore(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },[fetchData,page,activeTab]);
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ // 筛选数据
|
|
|
|
|
+ fetchData(1,'all').then((result)=> {
|
|
|
|
|
+
|
|
|
|
|
+ setListData(result);
|
|
|
|
|
+ // 判断是否有更多数据
|
|
|
|
|
+ setHasMore(!(result.length < PAGE_SIZE));
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ // 监听滚动:触底加载
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const handleScroll = () => {
|
|
|
|
|
+ if (loading || !hasMore) return;
|
|
|
|
|
+ const container = listRef.current;
|
|
|
|
|
+ // 滚动到底部的判断:滚动高度 + 可视高度 ≥ 总高度 - 20(阈值)
|
|
|
|
|
+ if (
|
|
|
|
|
+ container.scrollTop + container.clientHeight >=
|
|
|
|
|
+ container.scrollHeight - 20
|
|
|
|
|
+ ) {
|
|
|
|
|
+ loadMore();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const container = listRef.current;
|
|
|
|
|
+ if (container) {
|
|
|
|
|
+ container.addEventListener("scroll", handleScroll);
|
|
|
|
|
+ return () => container.removeEventListener("scroll", handleScroll);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [loading, hasMore, loadMore]);
|
|
|
|
|
+
|
|
|
|
|
+ const clickActiveTab = (tab: string) => {
|
|
|
|
|
+ setActiveTab(tab);
|
|
|
|
|
+ setPage(1);
|
|
|
|
|
+ fetchData(1, tab);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="min-h-screen bg-white">
|
|
|
|
|
+ {/* 顶部导航栏 */}
|
|
|
|
|
+ <div className="sticky top-0 z-10 bg-white border-b border-gray-100 py-4 px-4 flex items-center">
|
|
|
|
|
+ {/* 返回按钮 */}
|
|
|
|
|
+ <button className="mr-4">
|
|
|
|
|
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="black">
|
|
|
|
|
+ <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ {/* 标题 */}
|
|
|
|
|
+ <h1 className="text-xl font-bold text-black">Points Details</h1>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 标签切换栏 */}
|
|
|
|
|
+ <div className="flex border-b border-gray-200">
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => clickActiveTab("all")}
|
|
|
|
|
+ className={`px-6 py-3 font-medium text-sm ${
|
|
|
|
|
+ activeTab === "all"
|
|
|
|
|
+ ? "bg-black text-white"
|
|
|
|
|
+ : "bg-white text-gray-600 hover:bg-gray-50"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ ALL
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => clickActiveTab("rewarded")}
|
|
|
|
|
+ className={`px-6 py-3 font-medium text-sm ${
|
|
|
|
|
+ activeTab === "rewarded"
|
|
|
|
|
+ ? "bg-black text-white"
|
|
|
|
|
+ : "bg-white text-gray-600 hover:bg-gray-50"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ REWARDED
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => clickActiveTab("used")}
|
|
|
|
|
+ className={`px-6 py-3 font-medium text-sm ${
|
|
|
|
|
+ activeTab === "used"
|
|
|
|
|
+ ? "bg-black text-white"
|
|
|
|
|
+ : "bg-white text-gray-600 hover:bg-gray-50"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ USED
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 积分明细列表(带滚动监听) */}
|
|
|
|
|
+ <div ref={listRef} className="h-[calc(100vh-120px)] overflow-y-auto">
|
|
|
|
|
+ {listData.length > 0 ? (
|
|
|
|
|
+ <div className="divide-y divide-gray-100">
|
|
|
|
|
+ {listData.map((item) => (
|
|
|
|
|
+ <div key={item.id} className="px-4 py-4">
|
|
|
|
|
+ {/* 左侧:标题+日期 */}
|
|
|
|
|
+ <div className="flex justify-between items-start">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h3 className="text-base font-medium text-black">
|
|
|
|
|
+ {item.title}
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <p className="text-xs text-gray-500 mt-1">{item.date}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/* 右侧:积分变动 */}
|
|
|
|
|
+ <div className="text-right">
|
|
|
|
|
+ <p
|
|
|
|
|
+ className={`text-base font-medium ${
|
|
|
|
|
+ item.points.startsWith("+")
|
|
|
|
|
+ ? "text-black"
|
|
|
|
|
+ : "text-black"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ {item.points}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p className="text-xs text-gray-500 mt-1">
|
|
|
|
|
+ {item.currentPoints}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ // 空数据提示
|
|
|
|
|
+ <div className="flex items-center justify-center h-32">
|
|
|
|
|
+ <p className="text-gray-500 text-sm">NO DATA</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 加载中/无更多提示 */}
|
|
|
|
|
+ <div className="px-4 py-3 text-center text-sm">
|
|
|
|
|
+ {loading && <p className="text-gray-500">Loading...</p>}
|
|
|
|
|
+ {!loading && !hasMore && (
|
|
|
|
|
+ <p className="text-gray-500">NO MORE DATA</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export default PointsDetails;
|