diff --git a/src/App.tsx b/src/App.tsx index b51268c..3b9b392 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -// 你好。 +// 你好,感谢你愿意看源代码,但是悄悄告诉你,代码其实是AI写的所以质量很差喵。抱歉呜呜呜😭。 import React, { useState, useEffect, useRef, useCallback } from 'react'; import { FluentProvider, webLightTheme, webDarkTheme } from '@fluentui/react-components'; @@ -8,7 +8,7 @@ import MainLayout from './layouts/MainLayout'; import './App.css'; import { fetchArticles } from './api'; import CreatePost from './components/CreatePost'; -import { ToastContainer } from 'react-toastify'; +import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import AboutPage from './components/AboutPage'; import PostState from './components/PostState'; @@ -28,7 +28,19 @@ function App() { 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 touchStartYRef = useRef(null); + const pullDeltaRef = useRef(0); + const lastRefreshAtRef = useRef(0); + const [pullOffset, setPullOffset] = useState(0); + const [offsetAnimated, setOffsetAnimated] = useState(false); + const MAX_PULL = 80; // 最大下拉位移 + const TRIGGER_PULL = 60; // 触发刷新的阈值 + const DAMPING = 0.5; // 阻尼系数,减少位移幅度 + const REFRESH_COOLDOWN_MS = 5000; // 刷新冷却时间 const lastArticleRef = useCallback((node: HTMLDivElement) => { if (loading) return; @@ -41,6 +53,61 @@ function App() { if (node) observer.current.observe(node); }, [loading, hasMore]); + const doRefresh = () => { + 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; + }; + + const onTouchStart: React.TouchEventHandler = (e) => { + touchStartYRef.current = e.touches[0]?.clientY ?? null; + pullDeltaRef.current = 0; + setOffsetAnimated(false); + }; + + const onTouchMove: React.TouchEventHandler = (e) => { + const startY = touchStartYRef.current; + if (startY == null) return; + const currentY = e.touches[0]?.clientY ?? startY; + const rawDelta = currentY - startY; + pullDeltaRef.current = rawDelta; + const atTop = (containerRef.current?.scrollTop ?? 0) <= 0; + if (atTop && rawDelta > 0 && !loading && !refreshing) { + const offset = Math.min(MAX_PULL, rawDelta * DAMPING); + setPullOffset(offset); + } else { + setPullOffset(0); + } + }; + + const onTouchEnd: React.TouchEventHandler = () => { + const atTop = (containerRef.current?.scrollTop ?? 0) <= 0; + const shouldRefresh = atTop && pullOffset >= TRIGGER_PULL; + setOffsetAnimated(true); + setPullOffset(0); + if (shouldRefresh) { + doRefresh(); + } + touchStartYRef.current = null; + pullDeltaRef.current = 0; + // 结束后移除动画标记,下一次拖动为无动画的跟随效果 + setTimeout(() => setOffsetAnimated(false), 220); + }; + + const onWheel: React.WheelEventHandler = (e) => { + const atTop = (containerRef.current?.scrollTop ?? 0) <= 0; + if (atTop && e.deltaY < 0) { + doRefresh(); + } + }; + useEffect(() => { const controller = new AbortController(); const signal = controller.signal; @@ -61,6 +128,10 @@ function App() { } } finally { setLoading(false); + if (refreshing) { + setRefreshing(false); + toast.success('刷新成功!'); + } } }; loadArticles(); @@ -68,7 +139,7 @@ function App() { return () => { controller.abort(); }; - }, [page, hasMore]); + }, [page, hasMore, homeRefreshTick]); return ( @@ -78,8 +149,23 @@ function App() { -
+
+
+ {/* 刷新提示改为 toast,不显示顶部灰字 */} {articles.map((article, index) => { if (articles.length === index + 1 && hasMore) { return ( diff --git a/src/pages/InitPage.tsx b/src/pages/InitPage.tsx index 9f8a98f..076ddb0 100644 --- a/src/pages/InitPage.tsx +++ b/src/pages/InitPage.tsx @@ -84,7 +84,7 @@ const InitPage: React.FC = () => { setAdminToken(v?.value || '')} placeholder="请输入管理员令牌" /> - setUploadFolder(v?.value || '')} placeholder="例如:img" /> + setUploadFolder(v?.value || '')} placeholder="建议使用img" /> setAllowedExtensions(v?.value || '')} placeholder="png,jpg,jpeg,gif,webp" />