增加首页上拉刷新
This commit is contained in:
96
src/App.tsx
96
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<IntersectionObserver>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const touchStartYRef = useRef<number | null>(null);
|
||||
const pullDeltaRef = useRef<number>(0);
|
||||
const lastRefreshAtRef = useRef<number>(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<HTMLDivElement> = (e) => {
|
||||
touchStartYRef.current = e.touches[0]?.clientY ?? null;
|
||||
pullDeltaRef.current = 0;
|
||||
setOffsetAnimated(false);
|
||||
};
|
||||
|
||||
const onTouchMove: React.TouchEventHandler<HTMLDivElement> = (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<HTMLDivElement> = () => {
|
||||
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<HTMLDivElement> = (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 (
|
||||
<FluentProvider theme={isDarkMode ? webDarkTheme : webLightTheme}>
|
||||
@@ -78,8 +149,23 @@ function App() {
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<div style={{ width: '100%', height: 'calc(100vh - 64px)', overflowY: 'auto', padding: '20px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', minHeight: '100%' }}>
|
||||
<div
|
||||
style={{ width: '100%', height: 'calc(100vh - 64px)', overflowY: 'auto', padding: '20px' }}
|
||||
ref={containerRef}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
onTouchEnd={onTouchEnd}
|
||||
onWheel={onWheel}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
minHeight: '100%',
|
||||
transform: `translateY(${pullOffset}px)`,
|
||||
transition: offsetAnimated ? 'transform 200ms ease' : 'none'
|
||||
}}>
|
||||
{/* 刷新提示改为 toast,不显示顶部灰字 */}
|
||||
{articles.map((article, index) => {
|
||||
if (articles.length === index + 1 && hasMore) {
|
||||
return (
|
||||
|
||||
@@ -84,7 +84,7 @@ const InitPage: React.FC = () => {
|
||||
<Input value={adminToken} onChange={(_, v) => setAdminToken(v?.value || '')} placeholder="请输入管理员令牌" />
|
||||
</Field>
|
||||
<Field label="上传目录">
|
||||
<Input value={uploadFolder} onChange={(_, v) => setUploadFolder(v?.value || '')} placeholder="例如:img" />
|
||||
<Input value={uploadFolder} onChange={(_, v) => setUploadFolder(v?.value || '')} placeholder="建议使用img" />
|
||||
</Field>
|
||||
<Field label="允许扩展名 (逗号分隔)">
|
||||
<Input value={allowedExtensions} onChange={(_, v) => setAllowedExtensions(v?.value || '')} placeholder="png,jpg,jpeg,gif,webp" />
|
||||
|
||||
Reference in New Issue
Block a user