全新popiart网站 react项目重构
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

163 lines
6.1 KiB

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { getTemplateList } from "@/api/api";
import { EXPLORE_TEMPLATE_FEED_PANEL_ID } from "@/constants";
import { useComposerStore } from "@/store/useComposerStore";
import { useTemplateSelectStore } from "@/store/useMakeSameStore";
import { useImageDragTransfer, useWarmupImageDragUrls } from "@/hooks/useImageDragTransfer";
import type { ExploreTemplateItem } from "../types";
import { useTranslation } from "react-i18next";
import "./ExploreTemplateFeed.scss";
const FEED_PAGE_SIZE = 50;
const TEMPLATE_LOGO_SRC = new URL("/src/assets/images/popi_avatar.png", import.meta.url).href;
type TemplateListResponse = {
data?: {
list?: ExploreTemplateItem[];
};
};
/**
* 模板 Tab 内容区:请求、分页、加载态均内置,无 props。
*/
export default function ExploreTemplateFeed() {
const { t } = useTranslation();
const requestTemplateSelect = useTemplateSelectStore((s) => s.requestTemplateSelect);
const requestComposerExpand = useComposerStore((s) => s.requestComposerExpand);
const [items, setItems] = useState<ExploreTemplateItem[]>([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [initialLoading, setInitialLoading] = useState(false);
const [moreLoading, setMoreLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const bootstrappedRef = useRef(false);
const loadMoreRef = useRef<HTMLDivElement | null>(null);
const loadingMoreLockRef = useRef(false);
const loadedPageSetRef = useRef(new Set<number>());
const { getDragProps, warmupPreviewUrls } = useImageDragTransfer();
const previewUrls = useMemo(() => items.map((item) => item.coverUrl), [items]);
useWarmupImageDragUrls(warmupPreviewUrls, previewUrls);
const fetchFeed = useCallback(
async (nextPage: number, append: boolean) => {
if (append && (loadingMoreLockRef.current || loadedPageSetRef.current.has(nextPage))) return;
if (append) {
loadingMoreLockRef.current = true;
setMoreLoading(true);
} else {
loadedPageSetRef.current.clear();
setInitialLoading(true);
}
setError(null);
try {
const res = (await getTemplateList({ page: nextPage, pageSize: FEED_PAGE_SIZE })) as TemplateListResponse;
const list = Array.isArray(res?.data?.list) ? res.data.list : [];
setItems((prev) => {
if (!append) return list;
const knownIds = new Set(prev.map((item) => String(item.id)));
const added = list.filter((item) => !knownIds.has(String(item.id)));
return added.length === 0 ? prev : [...prev, ...added];
});
setHasMore(list.length >= FEED_PAGE_SIZE);
setPage(nextPage);
loadedPageSetRef.current.add(nextPage);
} catch (err) {
const msg = err instanceof Error ? err.message : t("pages.explore.feed.loadFailed");
setError(msg);
if (!append) setItems([]);
setHasMore(false);
} finally {
if (append) {
loadingMoreLockRef.current = false;
setMoreLoading(false);
} else {
setInitialLoading(false);
}
}
},
[t],
);
useEffect(() => {
if (bootstrappedRef.current) return;
bootstrappedRef.current = true;
void fetchFeed(1, false);
}, [fetchFeed]);
const loadingAny = initialLoading || moreLoading;
const handleUseTemplate = useCallback(
(item: ExploreTemplateItem) => {
const templateId = item?.id;
if (templateId == null || templateId === "") return;
requestTemplateSelect(templateId);
requestComposerExpand();
},
[requestComposerExpand, requestTemplateSelect],
);
useEffect(() => {
if (!hasMore || loadingAny) return;
const el = loadMoreRef.current;
if (el == null) return;
const obs = new IntersectionObserver(
(entries) => {
if (!entries.some((e) => e.isIntersecting)) return;
if (loadingMoreLockRef.current) return;
void fetchFeed(page + 1, true);
},
{ root: null, rootMargin: "240px", threshold: 0 },
);
obs.observe(el);
return () => {
obs.disconnect();
};
}, [hasMore, loadingAny, page, fetchFeed]);
return (
<div className="explorePage__feedPanel" id={EXPLORE_TEMPLATE_FEED_PANEL_ID} role="tabpanel" aria-labelledby="explore-feed-tab-template">
{error != null ? (
<p className="explorePage__feedError" role="alert">
{error}
</p>
) : null}
{initialLoading && items.length === 0 ? <p className="explorePage__feedLoading">{t("common.loading")}</p> : null}
<div className="explorePage__feedGrid">
{items.map((item) => {
return (
<article className="explorePage__tplCard" key={item.id} {...getDragProps(item.coverUrl)}>
<div className="explorePage__tplCardMedia">
<img src={item.coverUrl} alt="" />
</div>
<button
className="explorePage__tplCardUse"
type="button"
aria-label={t("pages.create.inspiration.templateLibrary.useTemplateAria")}
onClick={(evt) => {
evt.stopPropagation();
handleUseTemplate(item);
}}
>
<span className="explorePage__tplCardUseIcon" aria-hidden>
+
</span>
<span>{t("pages.explore.template.use")}</span>
</button>
<div className="explorePage__tplCardInfo">
<div className="explorePage__tplCardAvatar">
<img src={TEMPLATE_LOGO_SRC} alt="" />
</div>
<p className="explorePage__tplCardTitle">{item.title}</p>
</div>
</article>
);
})}
</div>
{hasMore && items.length > 0 ? <div className="explorePage__feedSentinel" ref={loadMoreRef} aria-hidden /> : null}
{moreLoading ? <p className="explorePage__feedLoading explorePage__feedLoading--more">{t("pages.explore.feed.loadingMore")}</p> : null}
</div>
);
}