import React, { useCallback, useEffect, useRef, useState } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { tokens, useToastController, Toast, ToastTitle } from '@fluentui/react-components'; import { MainLayout } from './layouts/MainLayout'; import About from './components/About'; import CreatePost from './components/CreatePost'; import PostCard from './components/PostCard'; import ImageViewer from './components/ImageViewer'; import { fetchArticles, type Article } from './api'; import { useLayout } from './context/LayoutContext'; import './App.css'; const Home: React.FC<{ onPreviewImage: (src: string, alt?: string) => void }> = ({ onPreviewImage }) => { const { refreshTrigger } = useLayout(); const { toasterId } = useLayout(); const { dispatchToast } = useToastController(toasterId); const [articles, setArticles] = useState([]); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const [homeRefreshTick, setHomeRefreshTick] = useState(0); const [refreshing, setRefreshing] = useState(false); const observer = useRef(null); const containerRef = useRef(null); const lastRefreshAtRef = useRef(0); const REFRESH_COOLDOWN_MS = 5000; const lastArticleRef = useCallback((node: HTMLDivElement | null) => { if (loading) return; if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver(entries => { if (entries[0].isIntersecting && hasMore) { setPage(prevPage => prevPage + 1); } }); if (node) observer.current.observe(node); }, [loading, hasMore]); const doRefresh = useCallback(() => { if (refreshing || loading) return; const now = Date.now(); if (now - lastRefreshAtRef.current < REFRESH_COOLDOWN_MS) return; lastRefreshAtRef.current = now; setRefreshing(true); setArticles([]); setHasMore(true); setPage(1); setHomeRefreshTick(t => t + 1); if (containerRef.current) containerRef.current.scrollTop = 0; }, [refreshing, loading]); const onWheel: React.WheelEventHandler = (e) => { const atTop = (containerRef.current?.scrollTop ?? 0) <= 0; if (atTop && e.deltaY < 0) { doRefresh(); } }; useEffect(() => { if (refreshTrigger > 0) { doRefresh(); } }, [refreshTrigger, doRefresh]); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; const loadArticles = async () => { if (!hasMore) return; setLoading(true); try { const newArticles = await fetchArticles(page, signal); if (newArticles.length === 0) { setHasMore(false); } else { setArticles(prev => [...prev, ...newArticles]); } } catch (error) { if (error instanceof Error && error.name !== 'AbortError') { console.error('Failed to load articles:', error); } } finally { setLoading(false); if (refreshing) { setRefreshing(false); dispatchToast( 刷新成功 , { intent: 'success' } ); } } }; loadArticles(); return () => controller.abort(); }, [page, hasMore, homeRefreshTick]); return (
{articles.map((article, index) => { if (articles.length === index + 1 && hasMore) { return (
); } return ( ); })} {loading &&
加载中...
} {!loading && !hasMore && (
已经到底了喵~
)}
); }; const NotFound = () =>

404 Not Found

; function App() { const [imageViewer, setImageViewer] = useState<{ open: boolean; src?: string; alt?: string }>({ open: false }); const openImageViewer = (src?: string, alt?: string) => { if (!src) return; setImageViewer({ open: true, src, alt }); }; const closeImageViewer = () => setImageViewer({ open: false }); return ( ) : null } /> } > } /> } /> } /> } /> ); } export default App;