feat: 实时监控改为瀑布流无限滚动+回到顶部
- 移除分页按钮,改为滚动触底自动加载下一页 - 滚动超过600px时显示蓝色回到顶部按钮 - 底部提示加载状态(加载中.../已加载全部 N 条) - 筛选/排序变化时自动重置为首页 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
import {
|
import {
|
||||||
Truck, Search, Filter, ChevronDown,
|
Truck, Search, Filter, ChevronDown,
|
||||||
Maximize2, Minimize2, RotateCcw,
|
Maximize2, Minimize2, RotateCcw,
|
||||||
ArrowUp, ArrowDown, ChevronLeft, ChevronRight,
|
ArrowUp, ArrowDown, ChevronsUp,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } from './types';
|
import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } from './types';
|
||||||
import { fetchMonitoring } from './api';
|
import { fetchMonitoring } from './api';
|
||||||
@@ -111,20 +111,24 @@ export default function MonitoringView() {
|
|||||||
const [filterOptions, setFilterOptions] = useState<MonitoringFilters>({ departments: [], customers: [], plates: [] });
|
const [filterOptions, setFilterOptions] = useState<MonitoringFilters>({ departments: [], customers: [], plates: [] });
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [updatedAt, setUpdatedAt] = useState('');
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [showBackToTop, setShowBackToTop] = useState(false);
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
const listEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const departments = filterOptions.departments;
|
const departments = filterOptions.departments;
|
||||||
const plateNumbers = filterOptions.plates;
|
const plateNumbers = filterOptions.plates;
|
||||||
const projects = filterOptions.customers;
|
const projects = filterOptions.customers;
|
||||||
|
|
||||||
const loadData = useCallback((p = page) => {
|
// 加载首页数据
|
||||||
|
const loadFirstPage = useCallback(() => {
|
||||||
fetchMonitoring({
|
fetchMonitoring({
|
||||||
sortBy,
|
sortBy,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
page: p,
|
page: 1,
|
||||||
search: searchTerm || undefined,
|
search: searchTerm || undefined,
|
||||||
dept: filterDept !== 'All' ? filterDept : undefined,
|
dept: filterDept !== 'All' ? filterDept : undefined,
|
||||||
customer: filterProject !== 'All' ? filterProject : undefined,
|
customer: filterProject !== 'All' ? filterProject : undefined,
|
||||||
@@ -133,22 +137,54 @@ export default function MonitoringView() {
|
|||||||
setStats(d.stats);
|
setStats(d.stats);
|
||||||
setFilterOptions(d.filters);
|
setFilterOptions(d.filters);
|
||||||
setTotal(d.total);
|
setTotal(d.total);
|
||||||
setPage(d.page);
|
|
||||||
setTotalPages(d.totalPages);
|
|
||||||
setUpdatedAt(d.updatedAt);
|
|
||||||
}).catch(() => {});
|
|
||||||
}, [sortBy, sortOrder, searchTerm, filterDept, filterProject, page]);
|
|
||||||
|
|
||||||
// 筛选/排序变化时重置到第1页
|
|
||||||
useEffect(() => {
|
|
||||||
setPage(1);
|
setPage(1);
|
||||||
loadData(1);
|
setHasMore(d.page < d.totalPages);
|
||||||
|
}).catch(() => {});
|
||||||
}, [sortBy, sortOrder, searchTerm, filterDept, filterProject]);
|
}, [sortBy, sortOrder, searchTerm, filterDept, filterProject]);
|
||||||
|
|
||||||
// 翻页时加载
|
// 加载更多
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (loadingMore || !hasMore) return;
|
||||||
|
const nextPage = page + 1;
|
||||||
|
setLoadingMore(true);
|
||||||
|
fetchMonitoring({
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
page: nextPage,
|
||||||
|
search: searchTerm || undefined,
|
||||||
|
dept: filterDept !== 'All' ? filterDept : undefined,
|
||||||
|
customer: filterProject !== 'All' ? filterProject : undefined,
|
||||||
|
}).then(d => {
|
||||||
|
setVehicles(prev => [...prev, ...d.vehicles]);
|
||||||
|
setPage(nextPage);
|
||||||
|
setHasMore(nextPage < d.totalPages);
|
||||||
|
}).catch(() => {}).finally(() => setLoadingMore(false));
|
||||||
|
}, [sortBy, sortOrder, searchTerm, filterDept, filterProject, page, loadingMore, hasMore]);
|
||||||
|
|
||||||
|
// 筛选/排序变化时重新加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData(page);
|
loadFirstPage();
|
||||||
}, [page]);
|
}, [loadFirstPage]);
|
||||||
|
|
||||||
|
// 滚动触底检测 + 回到顶部按钮
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
setShowBackToTop(scrollY > 600);
|
||||||
|
const scrollHeight = document.documentElement.scrollHeight;
|
||||||
|
const clientHeight = window.innerHeight;
|
||||||
|
if (scrollHeight - scrollY - clientHeight < 200) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, [loadMore]);
|
||||||
|
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
const filteredVehicles = vehicles;
|
const filteredVehicles = vehicles;
|
||||||
|
|
||||||
@@ -706,11 +742,11 @@ export default function MonitoringView() {
|
|||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between px-2 mb-1">
|
<div className="flex items-center justify-between px-2 mb-1">
|
||||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">车辆详情清单</span>
|
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">车辆详情清单</span>
|
||||||
<span className="text-[9px] font-bold text-slate-300">{filteredVehicles.length} 条</span>
|
<span className="text-[9px] font-bold text-slate-300">{total} 条</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-1.5">
|
<div className="grid grid-cols-1 gap-1.5">
|
||||||
{filteredVehicles.slice(0, 60).map((v) => (
|
{filteredVehicles.map((v) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
@@ -762,35 +798,40 @@ export default function MonitoringView() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredVehicles.length === 0 && (
|
{filteredVehicles.length === 0 && !loadingMore && (
|
||||||
<div className="py-10 text-center bg-white rounded-2xl border border-dashed border-slate-100">
|
<div className="py-10 text-center bg-white rounded-2xl border border-dashed border-slate-100">
|
||||||
<p className="text-xs font-bold text-slate-300">未找到匹配数据</p>
|
<p className="text-xs font-bold text-slate-300">未找到匹配数据</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 分页控件 */}
|
{/* 加载更多提示 */}
|
||||||
{totalPages > 1 && (
|
{loadingMore && (
|
||||||
<div className="flex items-center justify-center gap-3 py-3">
|
<div className="py-4 text-center">
|
||||||
<button
|
<span className="text-[10px] font-bold text-slate-400">加载中...</span>
|
||||||
onClick={() => setPage(Math.max(1, page - 1))}
|
|
||||||
disabled={page <= 1}
|
|
||||||
className="p-1.5 rounded-lg bg-white border border-slate-100 shadow-sm disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronLeft size={14} className="text-slate-600" />
|
|
||||||
</button>
|
|
||||||
<span className="text-[10px] font-bold text-slate-400">
|
|
||||||
{page} / {totalPages}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setPage(Math.min(totalPages, page + 1))}
|
|
||||||
disabled={page >= totalPages}
|
|
||||||
className="p-1.5 rounded-lg bg-white border border-slate-100 shadow-sm disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronRight size={14} className="text-slate-600" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!hasMore && filteredVehicles.length > 0 && (
|
||||||
|
<div className="py-4 text-center">
|
||||||
|
<span className="text-[10px] font-bold text-slate-300">已加载全部 {total} 条</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={listEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 回到顶部按钮 */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showBackToTop && (
|
||||||
|
<motion.button
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
|
onClick={scrollToTop}
|
||||||
|
className="fixed bottom-20 right-4 md:bottom-6 md:right-6 z-40 w-10 h-10 bg-blue-600 text-white rounded-full shadow-lg shadow-blue-200 flex items-center justify-center active:scale-95 transition-transform"
|
||||||
|
>
|
||||||
|
<ChevronsUp size={18} />
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user