|
@@ -0,0 +1,253 @@
|
|
|
|
|
+"use client";
|
|
|
|
|
+import Image from "next/image";
|
|
|
|
|
+import React, { useState, useEffect, useRef } 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;
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化/标签切换时重置数据
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ setPage(1);
|
|
|
|
|
+ setHasMore(true);
|
|
|
|
|
+ // 筛选数据
|
|
|
|
|
+ let filteredData = mockPointsData;
|
|
|
|
|
+ if (activeTab === 'rewarded') {
|
|
|
|
|
+ filteredData = mockPointsData.filter(item => item.type === 'rewarded');
|
|
|
|
|
+ } else if (activeTab === 'used') {
|
|
|
|
|
+ filteredData = mockPointsData.filter(item => item.type === 'used');
|
|
|
|
|
+ }
|
|
|
|
|
+ // 第一页数据
|
|
|
|
|
+ const firstPageData = filteredData.slice(0, PAGE_SIZE);
|
|
|
|
|
+ setListData(firstPageData);
|
|
|
|
|
+ // 判断是否有更多数据
|
|
|
|
|
+ setHasMore(filteredData.length > PAGE_SIZE);
|
|
|
|
|
+ }, [activeTab]);
|
|
|
|
|
+
|
|
|
|
|
+ // 监听滚动:触底加载
|
|
|
|
|
+ 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, listData]);
|
|
|
|
|
+
|
|
|
|
|
+ // 加载更多数据
|
|
|
|
|
+ const loadMore = () => {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ // 模拟接口请求延迟
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ let filteredData = mockPointsData;
|
|
|
|
|
+ if (activeTab === 'rewarded') {
|
|
|
|
|
+ filteredData = mockPointsData.filter(item => item.type === 'rewarded');
|
|
|
|
|
+ } else if (activeTab === 'used') {
|
|
|
|
|
+ filteredData = mockPointsData.filter(item => item.type === 'used');
|
|
|
|
|
+ }
|
|
|
|
|
+ // 计算下一页数据
|
|
|
|
|
+ const nextPage = page + 1;
|
|
|
|
|
+ const start = (nextPage - 1) * PAGE_SIZE;
|
|
|
|
|
+ const end = nextPage * PAGE_SIZE;
|
|
|
|
|
+ const newData = filteredData.slice(start, end);
|
|
|
|
|
+
|
|
|
|
|
+ if (newData.length > 0) {
|
|
|
|
|
+ setListData(prev => [...prev, ...newData]);
|
|
|
|
|
+ setPage(nextPage);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setHasMore(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }, 800);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ 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={() => setActiveTab('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={() => setActiveTab('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={() => setActiveTab('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;
|