|
|
@@ -2,7 +2,8 @@
|
|
|
import React, { useState, useRef, useEffect, useCallback } from "react";
|
|
|
import type {
|
|
|
ReviewTabType,
|
|
|
- ProductReviewList,
|
|
|
+ // ProductReviewList,
|
|
|
+ ProductReviewNode,
|
|
|
GalleryItem,
|
|
|
} from "@/types/products/review";
|
|
|
import ReviewImageGalleryModal from "./ReviewImageGalleryModal";
|
|
|
@@ -11,15 +12,28 @@ import { clientFetch } from "@/lib/restApiClient";
|
|
|
import { useCustomToast } from "@utils/hooks/useToast";
|
|
|
import { getUuId } from "@/utils/helper";
|
|
|
// 分页常量
|
|
|
-const PAGE_SIZE = 10;
|
|
|
+const PAGE_SIZE = 5;
|
|
|
|
|
|
interface ReviewModalProps {
|
|
|
- open: boolean;
|
|
|
+ // open: boolean;
|
|
|
+ reviewTotal:number,
|
|
|
onClose: () => void;
|
|
|
onChangeTab: (tabKey: string) => void;
|
|
|
productId: string;
|
|
|
// 父组件传入默认Tab all / newest / photos
|
|
|
activeTab: ReviewTabType;
|
|
|
+ galleryOpen: boolean;
|
|
|
+ galleryData: {
|
|
|
+ galleryList: GalleryItem[];
|
|
|
+ initialSlide: number;
|
|
|
+ };
|
|
|
+ galleryClose: () => void;
|
|
|
+ // 接收父传进来的打开弹窗函数
|
|
|
+ onOpenGallery: (
|
|
|
+ clickReviewIndex: number,
|
|
|
+ clickImgIndex: number,
|
|
|
+ reviewList: ProductReviewNode[]
|
|
|
+ ) => void;
|
|
|
}
|
|
|
// 模拟后端请求函数,替换为你的真实接口
|
|
|
async function fetchReviewApi(
|
|
|
@@ -27,93 +41,27 @@ async function fetchReviewApi(
|
|
|
tab: ReviewTabType,
|
|
|
pageNum: number,
|
|
|
guestId: string | null,
|
|
|
-): Promise<{ list: ProductReviewList; total: number }> {
|
|
|
- // 模拟网络延迟,模拟真实请求loading
|
|
|
- // await new Promise((resolve) => setTimeout(resolve, 700));
|
|
|
- // return {
|
|
|
- // list: [],
|
|
|
- // total: 100,
|
|
|
- // };
|
|
|
- // 全局固定100条模拟评论总数据
|
|
|
- // 2. 服务端 fetch 请求我们自己的 Next.js API
|
|
|
+ signal?: AbortSignal
|
|
|
+): Promise<{ list: ProductReviewNode[]; total: number }> {
|
|
|
+
|
|
|
let has_images = 0;
|
|
|
if (tab == "photos") {
|
|
|
has_images = 1;
|
|
|
}
|
|
|
const res = await clientFetch(
|
|
|
`/api/shop/products/${pid}/reviews?page=${pageNum}&per_page=${PAGE_SIZE}&has_images=${has_images}&sort=${tab}&client_id=${guestId}`,
|
|
|
+ {
|
|
|
+ signal
|
|
|
+ }
|
|
|
);
|
|
|
console.log(
|
|
|
"弹窗内res-=---------------------------------aa:",
|
|
|
res?.data,
|
|
|
);
|
|
|
-
|
|
|
- // const mockTotal = 100;
|
|
|
- // const fullMockData: ProductReviewList = [];
|
|
|
-
|
|
|
- // for (let i = 1; i <= mockTotal; i++) {
|
|
|
- // // 30条带图片评论,70条无图
|
|
|
- // const hasImage = i <= 30;
|
|
|
- // // 带图评论随机1~3张图片
|
|
|
- // const imgCount = hasImage ? Math.floor(Math.random() * 3) + 1 : 0;
|
|
|
- // const attachments: string[] = [];
|
|
|
- // for (let j = 1; j <= imgCount; j++) {
|
|
|
- // attachments.push(
|
|
|
- // `https://cdn.alipearlhair.com/media/reviewimages/cache/6/image/150x/040ec09b1e35df139433887a97daa66f/reviewimages/7O_A6W92AW3D_87N5PR8CO.png`,
|
|
|
- // );
|
|
|
- // }
|
|
|
-
|
|
|
- // // 随机星级1-5
|
|
|
- // const randomRating = Math.floor(Math.random() * 5) + 1;
|
|
|
- // // 随机标签
|
|
|
- // const tagArr: ("influencer" | "verified" | "")[] = [
|
|
|
- // "",
|
|
|
- // "verified",
|
|
|
- // "influencer",
|
|
|
- // ];
|
|
|
- // const randomTag = tagArr[Math.floor(Math.random() * tagArr.length)];
|
|
|
- // // 随机时间,用于Newest排序
|
|
|
- // const randomDayOffset = Math.floor(Math.random() * 90);
|
|
|
- // const createDate = new Date();
|
|
|
- // createDate.setDate(createDate.getDate() - randomDayOffset);
|
|
|
-
|
|
|
- // fullMockData.push({
|
|
|
- // __typename: "ProductReviewEdge",
|
|
|
- // node: {
|
|
|
- // name: `D****${i}`,
|
|
|
- // title: `${16 + Math.floor(Math.random() * 3)}`,
|
|
|
- // rating: randomRating,
|
|
|
- // comment:
|
|
|
- // "This lady is so sweetly gorgeous. I love the curl pattern, the side part makes it worth every penny spent. Hair is soft and natural, I bleached it the day she arrived.",
|
|
|
- // createdAt: createDate.toISOString(),
|
|
|
- // attachments,
|
|
|
- // isVip: true,
|
|
|
- // tag: randomTag,
|
|
|
- // likeCount: 1000 + Math.floor(Math.random() * 500),
|
|
|
- // },
|
|
|
- // });
|
|
|
- // }
|
|
|
// 根据tab筛选数据
|
|
|
const filteredList = [...res?.data];
|
|
|
const totalReview = res?.pagination?.total;
|
|
|
- // if (tab === "newest") {
|
|
|
- // // 最新:创建时间倒序
|
|
|
- // filteredList.sort(
|
|
|
- // (a, b) =>
|
|
|
- // new Date(b.node.createdAt).getTime() -
|
|
|
- // new Date(a.node.createdAt).getTime(),
|
|
|
- // );
|
|
|
- // } else if (tab === "photos") {
|
|
|
- // // 仅保留带图片评论
|
|
|
- // filteredList = filteredList.filter(
|
|
|
- // (item) => item.node.attachments.length > 0,
|
|
|
- // );
|
|
|
- // }
|
|
|
-
|
|
|
- // 分页切片
|
|
|
- // const startIndex = (pageNum - 1) * PAGE_SIZE;
|
|
|
- // const pageData = filteredList.slice(startIndex, startIndex + PAGE_SIZE);
|
|
|
-
|
|
|
+
|
|
|
return {
|
|
|
list: filteredList,
|
|
|
total: totalReview,
|
|
|
@@ -122,14 +70,19 @@ async function fetchReviewApi(
|
|
|
|
|
|
const ReviewModal: React.FC<ReviewModalProps> = React.memo(
|
|
|
function ReviewModal({
|
|
|
- open,
|
|
|
- onClose,
|
|
|
+ // open,
|
|
|
+ reviewTotal,
|
|
|
+ // onClose,
|
|
|
productId,
|
|
|
onChangeTab,
|
|
|
activeTab,
|
|
|
+ galleryOpen,
|
|
|
+ galleryData,
|
|
|
+ galleryClose,
|
|
|
+ onOpenGallery
|
|
|
}) {
|
|
|
// 全部原始评论数据(接口返回完整数据缓存)
|
|
|
- const [fullReviewList, setFullReviewList] = useState<ProductReviewList>([]);
|
|
|
+ const [fullReviewList, setFullReviewList] = useState<ProductReviewNode[]>([]);
|
|
|
// 分页状态
|
|
|
const [page, setPage] = useState(1);
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
@@ -137,17 +90,21 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
|
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
|
const { showToast } = useCustomToast();
|
|
|
// 图库弹窗状态
|
|
|
- const [galleryOpen, setGalleryOpen] = useState(false);
|
|
|
- const [galleryData, setGalleryData] = useState<{
|
|
|
- galleryList: GalleryItem[];
|
|
|
- initialSlide: number;
|
|
|
- }>({ galleryList: [], initialSlide: 0 });
|
|
|
+ // const [galleryOpen, setGalleryOpen] = useState(false);
|
|
|
+ const totalPage = Math.ceil(reviewTotal / PAGE_SIZE);
|
|
|
+ const [pageInputVal, setPageInputVal] = useState<string>(String(page));
|
|
|
+ // const [galleryData, setGalleryData] = useState<{
|
|
|
+ // galleryList: GalleryItem[];
|
|
|
+ // initialSlide: number;
|
|
|
+ // }>({ galleryList: [], initialSlide: 0 });
|
|
|
const [localLikeMap, setLocalLikeMap] = useState<
|
|
|
Record<string, { liked: boolean; like_count: number }>
|
|
|
>({});
|
|
|
const handleTabClick = useCallback(
|
|
|
(tabKey: ReviewTabType) => {
|
|
|
onChangeTab(tabKey);
|
|
|
+ setIsOpen(false);
|
|
|
+ setPageInputVal(String(1));
|
|
|
},
|
|
|
[onChangeTab],
|
|
|
);
|
|
|
@@ -155,108 +112,125 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
|
|
|
console.log("activeTab 变更:", activeTab);
|
|
|
}, [activeTab]);
|
|
|
|
|
|
- // ========== 弹窗打开 / Tab切换 / 分页 触发接口请求 ==========
|
|
|
+
|
|
|
const loadReviewData = useCallback(
|
|
|
- async (tab: ReviewTabType, pageNum: number, reset = false) => {
|
|
|
- setLoading(true);
|
|
|
- try {
|
|
|
- const uuid = getUuId();
|
|
|
- const res = await fetchReviewApi(productId, tab, pageNum, uuid);
|
|
|
- if (reset) {
|
|
|
- // 切换Tab/首次打开,重置列表
|
|
|
+ async (tab: ReviewTabType, pageNum: number) => {
|
|
|
+ const controller = new AbortController();
|
|
|
+ const signal = controller.signal;
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const uuid = getUuId();
|
|
|
+ // debugger
|
|
|
+ const res = await fetchReviewApi(productId, tab, pageNum, uuid, signal);
|
|
|
+ if (signal.aborted) return;
|
|
|
+ // 直接覆盖,不再追加
|
|
|
setFullReviewList(res.list);
|
|
|
- } else {
|
|
|
- // 触底分页,追加数据
|
|
|
- setFullReviewList((prev) => [...prev, ...res.list]);
|
|
|
+ setHasMore(pageNum * PAGE_SIZE < res.total);
|
|
|
+ } catch (err: any) {
|
|
|
+ if (err.name !== "AbortError") {
|
|
|
+ console.error("加载评论失败", err);
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ if (!signal.aborted) {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
}
|
|
|
- // 判断是否还有下一页
|
|
|
- setHasMore(pageNum * PAGE_SIZE < res.total);
|
|
|
- } catch (err) {
|
|
|
- console.error("加载评论失败", err);
|
|
|
- } finally {
|
|
|
- setLoading(false);
|
|
|
- }
|
|
|
- },
|
|
|
- [productId],
|
|
|
+ return () => controller.abort();
|
|
|
+ },
|
|
|
+ [productId],
|
|
|
);
|
|
|
// 切换Tab,重置分页、重新请求第一页
|
|
|
useEffect(() => {
|
|
|
- if (open) {
|
|
|
+ // if (open) {
|
|
|
setPage(1);
|
|
|
setHasMore(true);
|
|
|
- loadReviewData(activeTab, 1, true);
|
|
|
+ loadReviewData(activeTab, 1);
|
|
|
if (scrollContainerRef.current)
|
|
|
scrollContainerRef.current.scrollTop = 0;
|
|
|
- }
|
|
|
- }, [activeTab, open, loadReviewData]);
|
|
|
+ // }
|
|
|
+ }, [activeTab, loadReviewData]);
|
|
|
|
|
|
// 筛选当前Tab展示的评论
|
|
|
const filterReviewList = useCallback(() => {
|
|
|
const list = [...fullReviewList];
|
|
|
- // if (activeTab === "newest") {
|
|
|
- // list.sort(
|
|
|
- // (a, b) =>
|
|
|
- // new Date(b.node.createdAt).getTime() -
|
|
|
- // new Date(a.node.createdAt).getTime(),
|
|
|
- // );
|
|
|
- // } else if (activeTab === "photos") {
|
|
|
- // list = list.filter((item) => item.node.attachments.length > 0);
|
|
|
- // }
|
|
|
- // console.log("list----------------------------",list);
|
|
|
return list;
|
|
|
}, [activeTab, fullReviewList]);
|
|
|
|
|
|
// 当前分页渲染数据
|
|
|
- const displayList = filterReviewList(); //.slice(0, page * PAGE_SIZE)
|
|
|
-
|
|
|
+ const displayList = filterReviewList();
|
|
|
+ // debugger;
|
|
|
// 生成图库一维图片数组
|
|
|
- const buildGalleryList = useCallback((): GalleryItem[] => {
|
|
|
- const allGallery: GalleryItem[] = [];
|
|
|
- filterReviewList().forEach((edge: any, reviewIdx) => {
|
|
|
- edge.attachments.forEach((imgUrl: any) => {
|
|
|
- allGallery.push({
|
|
|
- reviewIndex: reviewIdx,
|
|
|
- imageUrl: imgUrl,
|
|
|
- review: edge,
|
|
|
- });
|
|
|
- });
|
|
|
- });
|
|
|
- return allGallery;
|
|
|
- }, [filterReviewList]);
|
|
|
-
|
|
|
- // 触底加载更多
|
|
|
- const handleScroll = useCallback(() => {
|
|
|
- if (!scrollContainerRef.current || loading || !hasMore) return;
|
|
|
- const el = scrollContainerRef.current;
|
|
|
- const isBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 100;
|
|
|
- if (isBottom) {
|
|
|
- const nextPage = page + 1;
|
|
|
- setPage(nextPage);
|
|
|
- loadReviewData(activeTab, nextPage, false);
|
|
|
+ // const buildGalleryList = useCallback((reviewList: any[]): GalleryItem[] => {
|
|
|
+ // const allGallery: GalleryItem[] = [];
|
|
|
+ // reviewList.forEach((edge: any, reviewIdx) => {
|
|
|
+ // edge.attachments.forEach((imgUrl: any) => {
|
|
|
+ // allGallery.push({
|
|
|
+ // reviewIndex: reviewIdx,
|
|
|
+ // imageUrl: imgUrl,
|
|
|
+ // review: edge,
|
|
|
+ // });
|
|
|
+ // });
|
|
|
+ // });
|
|
|
+ // return allGallery;
|
|
|
+ // }, []);
|
|
|
+ // 页码切换
|
|
|
+ const handlePageChange = (newPage: number) => {
|
|
|
+ // 边界校验
|
|
|
+ if (newPage < 1 || newPage > totalPage) return;
|
|
|
+ setPage(newPage);
|
|
|
+ setPageInputVal(String(newPage));
|
|
|
+ // 重置滚动到顶部
|
|
|
+ scrollContainerRef.current?.scrollTo({ top: 0, behavior: "smooth" });
|
|
|
+ // 触发接口请求,加载对应页评论(你原有loadReviews逻辑)
|
|
|
+ // fetchReviews(newPage);
|
|
|
+ loadReviewData(activeTab, newPage);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 输入框回车/失焦跳转
|
|
|
+ const handlePageInputSubmit = () => {
|
|
|
+ const num = Number(pageInputVal);
|
|
|
+ if (Number.isInteger(num) && num >= 1 && num <= totalPage) {
|
|
|
+ handlePageChange(num);
|
|
|
+ } else {
|
|
|
+ // 非法值回退当前页
|
|
|
+ setPageInputVal(String(page));
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 渲染中间页码(只展示当前附近页码,和截图一致最多5个页码按钮)
|
|
|
+ const renderPageNumbers = () => {
|
|
|
+ const pages: number[] = [];
|
|
|
+ let start = Math.max(1, page - 2);
|
|
|
+ const end = Math.min(totalPage, start + 4);
|
|
|
+ // 修正,保证最多5个
|
|
|
+ if(end - start <4) start = Math.max(1, end -4);
|
|
|
+ for(let i=start; i<=end; i++) pages.push(i);
|
|
|
+ return pages;
|
|
|
}
|
|
|
- }, [loading, hasMore, page, activeTab, loadReviewData]);
|
|
|
|
|
|
// 打开图片预览弹窗,定位点击图片下标
|
|
|
- const openGalleryModal = (
|
|
|
- clickReviewIndex: number,
|
|
|
- clickImgIndex: number,
|
|
|
- ) => {
|
|
|
- console.log(
|
|
|
- "clickReviewIndex:",
|
|
|
- clickReviewIndex,
|
|
|
- "clickImgIndex:",
|
|
|
- clickImgIndex,
|
|
|
- );
|
|
|
+ // const openGalleryModal = (
|
|
|
+ // clickReviewIndex: number,
|
|
|
+ // clickImgIndex: number,
|
|
|
+ // reviewList: ProductReviewList
|
|
|
+ // ) => {
|
|
|
+ // console.log(
|
|
|
+ // "clickReviewIndex:",
|
|
|
+ // clickReviewIndex,
|
|
|
+ // "clickImgIndex:",
|
|
|
+ // clickImgIndex,
|
|
|
+ // );
|
|
|
|
|
|
- const galleryAll = buildGalleryList();
|
|
|
- const targetReview: any = filterReviewList()[clickReviewIndex];
|
|
|
- const targetImg = targetReview.attachments[clickImgIndex];
|
|
|
- const targetSlide = galleryAll.findIndex(
|
|
|
- (g) => g.reviewIndex === clickReviewIndex && g.imageUrl === targetImg,
|
|
|
- );
|
|
|
- setGalleryData({ galleryList: galleryAll, initialSlide: targetSlide });
|
|
|
- setGalleryOpen(true);
|
|
|
- };
|
|
|
+ // const galleryAll = buildGalleryList(reviewList);
|
|
|
+ // const targetReview: any = reviewList[clickReviewIndex];
|
|
|
+ // const targetImg = targetReview.attachments[clickImgIndex];
|
|
|
+
|
|
|
+ // const targetSlide = galleryAll.findIndex(
|
|
|
+ // (g) => g.reviewIndex === clickReviewIndex && g.imageUrl === targetImg
|
|
|
+ // );
|
|
|
+ // setGalleryData({ galleryList: galleryAll, initialSlide: targetSlide });
|
|
|
+ // setGalleryOpen(true);
|
|
|
+ // };
|
|
|
const getCurrentLike = (item: any) => {
|
|
|
const cache = localLikeMap[item.id];
|
|
|
if (cache) return cache;
|
|
|
@@ -324,16 +298,34 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
|
|
|
showToast("点赞失败,请稍后重试");
|
|
|
}
|
|
|
};
|
|
|
-
|
|
|
- if (!open) return null;
|
|
|
-
|
|
|
+// 映射显示文本
|
|
|
+const tabLabelMap: Record<ReviewTabType, string> = {
|
|
|
+ all: "ALL",
|
|
|
+ newest: "Newest",
|
|
|
+ photos: "Photos",
|
|
|
+};
|
|
|
+ // if (!open) return null;
|
|
|
+ const [isOpen, setIsOpen] = useState(false);
|
|
|
+ const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
+ // 点击外部关闭下拉
|
|
|
+ useEffect(() => {
|
|
|
+ const handleClickOutside = (event: MouseEvent) => {
|
|
|
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
|
+ setIsOpen(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ document.addEventListener("mousedown", handleClickOutside);
|
|
|
+ return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
|
+ }, []);
|
|
|
+
|
|
|
return (
|
|
|
<>
|
|
|
{/* 遮罩层 */}
|
|
|
- <div className="fixed inset-0 bg-black/60 z-[90] flex items-center justify-center ">
|
|
|
- <div className="w-full max-w-3xl h-full bg-white flex flex-col overflow-hidden">
|
|
|
+ <div className="w-full flex items-center justify-center ">
|
|
|
+ {/* //fixed inset-0 bg-black/60 z-[90] */}
|
|
|
+ <div className="w-full h-full bg-white flex flex-col overflow-hidden">
|
|
|
{/* 顶部固定头部:返回 + Reviews标题 */}
|
|
|
- <div className="flex items-center justify-between p-4 border-b border-gray-200 shrink-0">
|
|
|
+ {/* <div className="flex items-center justify-between p-4 border-b border-gray-200 shrink-0">
|
|
|
<button onClick={onClose} className="shrink-0">
|
|
|
<svg
|
|
|
width="24"
|
|
|
@@ -349,10 +341,10 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
|
|
|
</button>
|
|
|
<h2 className="text-2xl font-semibold">Reviews</h2>
|
|
|
<div className="w-6 shrink-0"></div>
|
|
|
- </div>
|
|
|
+ </div> */}
|
|
|
|
|
|
{/* Tab切换栏 固定 */}
|
|
|
- <div className="flex gap-3 px-4 py-3 border-b border-gray-200 shrink-0">
|
|
|
+ {/* <div className="flex gap-3 px-4 py-3 border-b border-gray-200 shrink-0">
|
|
|
{(["all", "newest", "photos"] as ReviewTabType[]).map((tab) => (
|
|
|
<button
|
|
|
key={tab}
|
|
|
@@ -370,13 +362,53 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
|
|
|
: "Photos"}
|
|
|
</button>
|
|
|
))}
|
|
|
+ </div> */}
|
|
|
+ <div className="flex items-center justify-between py-3 border-b border-gray-200 shrink-0">
|
|
|
+ {/* 左侧 Reviews (数量) */}
|
|
|
+ <div className="text-xl font-medium">Reviews ({reviewTotal})</div>
|
|
|
+
|
|
|
+ {/* 右侧下拉选择器 */}
|
|
|
+ <div ref={dropdownRef} className="relative">
|
|
|
+ {/* 触发器:文字+箭头 */}
|
|
|
+ <button
|
|
|
+ onClick={() => setIsOpen(!isOpen)}
|
|
|
+ className="flex items-center gap-2 text-lg cursor-pointer"
|
|
|
+ >
|
|
|
+ {tabLabelMap[activeTab]}
|
|
|
+ <svg
|
|
|
+ className={`w-4 h-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
|
|
+ fill="none"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ stroke="currentColor"
|
|
|
+ >
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ {/* 下拉菜单 */}
|
|
|
+ {isOpen && (
|
|
|
+ <div className="absolute right-0 mt-2 w-36 bg-white shadow-lg rounded-md z-20 border border-gray-200">
|
|
|
+ {(["all", "newest", "photos"] as ReviewTabType[]).map((tab) => (
|
|
|
+ <button
|
|
|
+ key={tab}
|
|
|
+ onClick={() => handleTabClick(tab)}
|
|
|
+ className={`w-full text-left px-4 py-2 hover:bg-gray-100 ${
|
|
|
+ activeTab === tab ? "font-semibold" : ""
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ {tabLabelMap[tab]}
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
+ </div>
|
|
|
|
|
|
{/* 可滚动评论列表区域 */}
|
|
|
<div
|
|
|
ref={scrollContainerRef}
|
|
|
- onScroll={handleScroll}
|
|
|
- className="flex-1 overflow-y-auto px-4 py-4 space-y-6"
|
|
|
+ // onScroll={handleScroll}
|
|
|
+ className="flex-1 overflow-y-auto py-4 space-y-6"
|
|
|
>
|
|
|
{loading && page === 1 ? (
|
|
|
<p className="text-center py-10 text-gray-400">
|
|
|
@@ -385,7 +417,9 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
|
|
|
) : displayList.length === 0 ? (
|
|
|
<p className="text-center text-gray-500 py-10">No reviews</p>
|
|
|
) : (
|
|
|
- displayList.map((item: any, reviewIdx) => {
|
|
|
+ displayList.map((edge: ProductReviewNode, reviewIdx) => {
|
|
|
+ const item = edge;
|
|
|
+ // debugger;
|
|
|
const { liked, like_count } = getCurrentLike(item);
|
|
|
// const item = edge.node;
|
|
|
return (
|
|
|
@@ -508,9 +542,7 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
|
|
|
{item.attachments.map((img: any, imgIdx: any) => (
|
|
|
<button
|
|
|
key={imgIdx}
|
|
|
- onClick={() =>
|
|
|
- openGalleryModal(reviewIdx, imgIdx)
|
|
|
- }
|
|
|
+ onClick={() => onOpenGallery(reviewIdx, imgIdx, displayList)}
|
|
|
className="w-28 h-28 rounded-lg overflow-hidden"
|
|
|
>
|
|
|
<Image
|
|
|
@@ -530,7 +562,7 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
|
|
|
)}
|
|
|
{loading && page > 1 && (
|
|
|
<p className="text-center py-4 text-gray-400">
|
|
|
- Loading more...
|
|
|
+ Loading ...
|
|
|
</p>
|
|
|
)}
|
|
|
{!hasMore && displayList.length > 0 && (
|
|
|
@@ -541,17 +573,81 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
{/* 图片预览弹窗 完全不变 */}
|
|
|
<ReviewImageGalleryModal
|
|
|
open={galleryOpen}
|
|
|
- onClose={() => setGalleryOpen(false)}
|
|
|
+ onClose={galleryClose}
|
|
|
galleryList={galleryData.galleryList}
|
|
|
initialSlide={galleryData.initialSlide}
|
|
|
- //直接绑定父原有点赞函数,不需要任何修改
|
|
|
onLike={handleLikeClick}
|
|
|
getCurrentLike={getCurrentLike}
|
|
|
/>
|
|
|
+ <div className="flex items-center justify-center gap-3 py-6 shrink-0">
|
|
|
+ <button
|
|
|
+ onClick={()=>handlePageChange(page -1)}
|
|
|
+ disabled={page <= 1}
|
|
|
+ className={`px-1 py-1 ${page <=1 ? "opacity-40 cursor-not-allowed" : "cursor-pointer"}`}
|
|
|
+ >
|
|
|
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
|
+ <polyline points="15 18 9 12 15 6"></polyline>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ {renderPageNumbers().map(p=> (
|
|
|
+ <button
|
|
|
+ key={p}
|
|
|
+ onClick={()=>handlePageChange(p)}
|
|
|
+ className={`w-9 h-9 rounded-full text-base transition-colors
|
|
|
+ ${page === p
|
|
|
+ ? "bg-[#0F6636] text-white"
|
|
|
+ : "hover:bg-gray-100 text-gray-700"}
|
|
|
+ `}
|
|
|
+ >
|
|
|
+ {p}
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {/* 下一页 */}
|
|
|
+ <button
|
|
|
+ onClick={()=>handlePageChange(page +1)}
|
|
|
+ disabled={page >= totalPage}
|
|
|
+ className={`px-1 py-1 ${page >= totalPage ? "opacity-40 cursor-not-allowed" : "cursor-pointer"}`}
|
|
|
+ >
|
|
|
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
|
+ <polyline points="9 18 15 12 9 6"></polyline>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+
|
|
|
+
|
|
|
+ <div className="flex items-center ml-4">
|
|
|
+ <button
|
|
|
+ onClick={()=>handlePageChange(page -1)}
|
|
|
+ disabled={page <=1}
|
|
|
+ className={`px-1 ${page <=1 ? "opacity-40" : ""}`}
|
|
|
+ >
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
|
+ <polyline points="15 18 9 12 15 6"></polyline>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <input
|
|
|
+ value={pageInputVal}
|
|
|
+ onChange={(e)=>setPageInputVal(e.target.value)}
|
|
|
+ onKeyDown={(e)=> e.key === 'Enter' && handlePageInputSubmit()}
|
|
|
+ onBlur={handlePageInputSubmit}
|
|
|
+ className="w-14 h-9 border border-gray-300 rounded-md text-center outline-none focus:border-[#0F6636]"
|
|
|
+ type="text"
|
|
|
+ />
|
|
|
+ <span className="mx-1">/ {totalPage}</span>
|
|
|
+ <button
|
|
|
+ onClick={()=>handlePageChange(page +1)}
|
|
|
+ disabled={page >= totalPage}
|
|
|
+ className={`px-1 ${page >= totalPage ? "opacity-40" : ""}`}
|
|
|
+ >
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
|
+ <polyline points="9 18 15 12 9 6"></polyline>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</>
|
|
|
);
|
|
|
},
|