[feat]:[FL-85][菜单管理页面卡片视图添加无限滚动加载功能]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-19 17:06:06 +08:00
parent 810674b389
commit c16b1d9fc1
+86 -10
View File
@@ -106,6 +106,9 @@ export default function AdminMenusPage() {
const [activeStatusFilter, setActiveStatusFilter] = useState<FilterStatus>("all");
const [tableScrollY, setTableScrollY] = useState(MENU_TABLE_MIN_SCROLL_Y);
const viewMode: "table" | "card" = isMobile ? "card" : "table";
const [cardViewPage, setCardViewPage] = useState(1);
const [allLoadedMenus, setAllLoadedMenus] = useState<MenuItem[]>([]);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [form] = Form.useForm<MenuFormValues>();
const tableScrollAnchorRef = useRef<HTMLDivElement | null>(null);
const keywordDebounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -145,7 +148,7 @@ export default function AdminMenusPage() {
});
}, [menus]);
const loadMenus = useCallback(async () => {
const loadMenus = useCallback(async (page = 1, pageSize = 20) => {
if (!canRead) {
setLoading(false);
return;
@@ -155,6 +158,8 @@ export default function AdminMenusPage() {
setError("");
const params = new URLSearchParams();
params.append("limit", String(pageSize));
params.append("offset", String((page - 1) * pageSize));
if (activeKeyword.trim()) {
params.append("keyword", activeKeyword.trim());
}
@@ -173,6 +178,7 @@ export default function AdminMenusPage() {
const payload = (await response.json()) as MenuListResponse;
setMenus(payload.items.map(normalizeMenuItemPath));
setLoading(false);
return payload;
}, [canRead, fetchWithAuth, activeKeyword, activeStatusFilter]);
useEffect(() => {
@@ -194,6 +200,62 @@ export default function AdminMenusPage() {
}, [canRead, loadMenus, user]),
);
// Update allLoadedMenus when menus data changes in card view
useEffect(() => {
if (viewMode === "card" && !loading) {
if (cardViewPage === 1) {
setAllLoadedMenus(menus);
} else {
setAllLoadedMenus((prev) => {
if (menus.length === 0) {
return prev;
}
const existingIds = new Set(prev.map(m => m.id));
const newMenus = menus.filter(m => !existingIds.has(m.id));
return [...prev, ...newMenus];
});
}
setIsLoadingMore(false);
}
}, [menus, loading, viewMode, cardViewPage]);
// Handle infinite scroll for card view
useEffect(() => {
if (viewMode !== "card") return;
const pageCard = pageCardRef.current;
if (!pageCard) return;
const cardBody = pageCard.querySelector<HTMLElement>(".ant-card-body");
if (!cardBody) return;
const handleScroll = () => {
if (isLoadingMore || loading) return;
const scrollTop = cardBody.scrollTop;
const scrollHeight = cardBody.scrollHeight;
const clientHeight = cardBody.clientHeight;
if (scrollTop + clientHeight >= scrollHeight - 100) {
setIsLoadingMore(true);
setCardViewPage((prev) => {
const nextPage = prev + 1;
void loadMenus(nextPage, 20);
return nextPage;
});
}
};
cardBody.addEventListener("scroll", handleScroll);
return () => cardBody.removeEventListener("scroll", handleScroll);
}, [viewMode, isLoadingMore, loading, loadMenus, allLoadedMenus.length]);
// Reset card view state when search conditions change
useEffect(() => {
setCardViewPage(1);
setAllLoadedMenus([]);
}, [activeStatusFilter, activeKeyword]);
const handleSearch = useCallback(() => {
setActiveKeyword(keyword);
setActiveStatusFilter(statusFilter);
@@ -781,11 +843,11 @@ export default function AdminMenusPage() {
</div>
) : (
<div className="admin-menus-card-view mt-4">
{loading && filteredMenus.length === 0 ? (
{loading && allLoadedMenus.length === 0 ? (
<div style={{ textAlign: "center", padding: "60px 0" }}>
<Spin tip="加载中..." />
</div>
) : filteredMenus.length === 0 ? (
) : allLoadedMenus.length === 0 ? (
<div style={{ textAlign: "center", padding: "60px 0" }}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
@@ -793,13 +855,27 @@ export default function AdminMenusPage() {
/>
</div>
) : (
<Row gutter={[12, 12]}>
{filteredMenus.map((menuItem) => (
<Col key={menuItem.id} xs={24} sm={24} md={12} lg={8} xl={6}>
{renderMenuCard(menuItem)}
</Col>
))}
</Row>
<>
<Row gutter={[12, 12]}>
{allLoadedMenus.map((menuItem) => (
<Col key={menuItem.id} xs={24} sm={24} md={12} lg={8} xl={6}>
{renderMenuCard(menuItem)}
</Col>
))}
</Row>
{isLoadingMore && (
<div style={{ textAlign: "center", padding: "20px 0" }}>
<Spin tip="加载更多..." />
</div>
)}
{!loading && !isLoadingMore && allLoadedMenus.length > 0 && (
<div style={{ textAlign: "center", padding: "20px 0" }}>
<Typography.Text type="secondary">
{allLoadedMenus.length}
</Typography.Text>
</div>
)}
</>
)}
</div>
)}