fix: show mileage query loading animation and bump version to 1.1.15
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
shishengliang
2026-09-20 10:07:08 +08:00
parent b15951f08b
commit 61406497cc
6 changed files with 66 additions and 51 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ln-bi", "name": "ln-bi",
"version": "1.1.14", "version": "1.1.15",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ln-bi", "name": "ln-bi",
"version": "1.1.14", "version": "1.1.15",
"dependencies": { "dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1", "@amap/amap-jsapi-loader": "^1.0.1",
"@hono/node-server": "^1.13.0", "@hono/node-server": "^1.13.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "ln-bi", "name": "ln-bi",
"private": true, "private": true,
"version": "1.1.14", "version": "1.1.15",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"", "dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"",
@@ -0,0 +1,44 @@
import { RefreshCw } from 'lucide-react';
import { motion } from 'motion/react';
export default function MileageLoadingOverlay({ title = '正在查询里程', description = '正在加载所选条件的里程数据,请稍候' }: { title?: string; description?: string }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.16, ease: 'easeOut' }}
role="status"
aria-live="polite"
className="fixed inset-0 z-[100] flex items-center justify-center bg-slate-950/20 p-5 backdrop-blur-[2px]"
>
<motion.div
initial={{ opacity: 0, y: 12, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 8, scale: 0.98 }}
transition={{ duration: 0.22, ease: 'easeOut' }}
className="w-full max-w-[260px] overflow-hidden rounded-xl border border-slate-100 bg-white p-5 text-center shadow-2xl"
>
<div className="relative mx-auto flex h-12 w-12 items-center justify-center">
<motion.span
className="absolute inset-0 rounded-full border-2 border-blue-100"
animate={{ scale: [0.86, 1.15], opacity: [0.9, 0] }}
transition={{ duration: 1.2, repeat: Infinity, ease: 'easeOut' }}
/>
<span className="relative flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600">
<RefreshCw size={19} className="animate-spin" />
</span>
</div>
<div className="mt-3 text-sm font-black text-slate-900">{title}</div>
<div className="mt-1 text-[11px] font-bold text-slate-400">{description}</div>
<div className="mt-4 h-1 overflow-hidden rounded-full bg-slate-100">
<motion.div
className="h-full w-1/2 rounded-full bg-blue-500"
animate={{ x: ['-100%', '220%'] }}
transition={{ duration: 1.15, repeat: Infinity, ease: 'easeInOut' }}
/>
</div>
</motion.div>
</motion.div>
);
}
+15 -5
View File
@@ -12,6 +12,7 @@ import type {
import { fetchMonitoring } from './api'; import { fetchMonitoring } from './api';
import { exportMileageXlsx } from './xlsx-export'; import { exportMileageXlsx } from './xlsx-export';
import VehicleDetailModal from './VehicleDetailModal'; import VehicleDetailModal from './VehicleDetailModal';
import MileageLoadingOverlay from './MileageLoadingOverlay';
import { import {
defaultMileageDate, defaultMileageDate,
getRangePreset, getRangePreset,
@@ -148,10 +149,14 @@ export default function MonitoringView() {
return inAlertTarget && Math.max(0, v.dailyKm || 0) >= HIGH_MILEAGE_ALERT_KM; return inAlertTarget && Math.max(0, v.dailyKm || 0) >= HIGH_MILEAGE_ALERT_KM;
}, [filterTargetNames]); }, [filterTargetNames]);
const queryRequestRef = useRef(0);
// 加载首页数据 // 加载首页数据
const loadFirstPage = useCallback((showPageLoading = true) => { const loadFirstPage = useCallback((showPageLoading = true) => {
const requestId = ++queryRequestRef.current;
if (showPageLoading) setPageLoading(true); if (showPageLoading) setPageLoading(true);
return fetchMonitoring(buildInitialMonitoringQuery(monitoringQuery, PAGE_SIZE)).then(d => { return fetchMonitoring(buildInitialMonitoringQuery(monitoringQuery, PAGE_SIZE)).then(d => {
if (requestId !== queryRequestRef.current) return;
setVehicles(d.vehicles); setVehicles(d.vehicles);
setStats(d.stats); setStats(d.stats);
setFilterOptions(d.filters); setFilterOptions(d.filters);
@@ -162,31 +167,33 @@ export default function MonitoringView() {
setHasMore(d.page < d.totalPages); setHasMore(d.page < d.totalPages);
setLastRefreshedAt(new Date()); setLastRefreshedAt(new Date());
}).catch(() => {}).finally(() => { }).catch(() => {}).finally(() => {
if (showPageLoading) setPageLoading(false); if (requestId === queryRequestRef.current) setPageLoading(false);
}); });
}, [monitoringQuery, rangeStart, rangeEnd]); }, [monitoringQuery, rangeStart, rangeEnd]);
const handleManualRefresh = useCallback(async () => { const handleManualRefresh = useCallback(async () => {
if (manualRefreshing) return; if (manualRefreshing || pageLoading) return;
setManualRefreshing(true); setManualRefreshing(true);
try { try {
await loadFirstPage(false); await loadFirstPage(false);
} finally { } finally {
setManualRefreshing(false); setManualRefreshing(false);
} }
}, [loadFirstPage, manualRefreshing]); }, [loadFirstPage, manualRefreshing, pageLoading]);
// 加载更多 // 加载更多
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
if (loadingMore || !hasMore) return; if (pageLoading || loadingMore || !hasMore) return;
const requestId = queryRequestRef.current;
const nextPage = page + 1; const nextPage = page + 1;
setLoadingMore(true); setLoadingMore(true);
fetchMonitoring(buildNextPageMonitoringQuery(monitoringQuery, PAGE_SIZE, nextPage)).then(d => { fetchMonitoring(buildNextPageMonitoringQuery(monitoringQuery, PAGE_SIZE, nextPage)).then(d => {
if (requestId !== queryRequestRef.current) return;
setVehicles(prev => [...prev, ...d.vehicles]); setVehicles(prev => [...prev, ...d.vehicles]);
setPage(nextPage); setPage(nextPage);
setHasMore(nextPage < d.totalPages); setHasMore(nextPage < d.totalPages);
}).catch(() => {}).finally(() => setLoadingMore(false)); }).catch(() => {}).finally(() => setLoadingMore(false));
}, [monitoringQuery, page, loadingMore, hasMore]); }, [monitoringQuery, page, pageLoading, loadingMore, hasMore]);
// 筛选/排序变化时重新加载 // 筛选/排序变化时重新加载
useEffect(() => { useEffect(() => {
@@ -302,6 +309,9 @@ export default function MonitoringView() {
return ( return (
<> <>
<AnimatePresence>
{pageLoading && <MileageLoadingOverlay />}
</AnimatePresence>
{/* 顶部哨兵:离开视口时显示回到顶部按钮 */} {/* 顶部哨兵:离开视口时显示回到顶部按钮 */}
<div ref={topSentinelRef} className="h-0" /> <div ref={topSentinelRef} className="h-0" />
@@ -1,45 +1,6 @@
import { RefreshCw } from 'lucide-react'; import MileageLoadingOverlay from '../MileageLoadingOverlay';
import { motion } from 'motion/react';
import { fmtDate } from './utils'; import { fmtDate } from './utils';
export default function DailyReportLoadingOverlay({ reportDate }: { reportDate: string }) { export default function DailyReportLoadingOverlay({ reportDate }: { reportDate: string }) {
return ( return <MileageLoadingOverlay title="正在加载日报" description={`正在汇总 ${fmtDate(reportDate)} 的里程数据`} />;
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.16, ease: 'easeOut' }}
role="status"
aria-live="polite"
className="fixed inset-0 z-[100] flex items-center justify-center bg-slate-950/20 p-5 backdrop-blur-[2px]"
>
<motion.div
initial={{ opacity: 0, y: 12, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 8, scale: 0.98 }}
transition={{ duration: 0.22, ease: 'easeOut' }}
className="w-full max-w-[260px] overflow-hidden rounded-xl border border-slate-100 bg-white p-5 text-center shadow-2xl"
>
<div className="relative mx-auto flex h-12 w-12 items-center justify-center">
<motion.span
className="absolute inset-0 rounded-full border-2 border-blue-100"
animate={{ scale: [0.86, 1.15], opacity: [0.9, 0] }}
transition={{ duration: 1.2, repeat: Infinity, ease: 'easeOut' }}
/>
<span className="relative flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600">
<RefreshCw size={19} className="animate-spin" />
</span>
</div>
<div className="mt-3 text-sm font-black text-slate-900"></div>
<div className="mt-1 text-[11px] font-bold text-slate-400"> {fmtDate(reportDate)} </div>
<div className="mt-4 h-1 overflow-hidden rounded-full bg-slate-100">
<motion.div
className="h-full w-1/2 rounded-full bg-blue-500"
animate={{ x: ['-100%', '220%'] }}
transition={{ duration: 1.15, repeat: Infinity, ease: 'easeInOut' }}
/>
</div>
</motion.div>
</motion.div>
);
} }
@@ -134,7 +134,7 @@ export default function VehicleList({
})} })}
</div> </div>
{vehicles.length === 0 && !loadingMore && ( {vehicles.length === 0 && !pageLoading && !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>
@@ -146,7 +146,7 @@ export default function VehicleList({
<span className="text-[10px] font-bold text-slate-400">...</span> <span className="text-[10px] font-bold text-slate-400">...</span>
</div> </div>
)} )}
{!hasMore && vehicles.length > 0 && ( {!pageLoading && !hasMore && vehicles.length > 0 && (
<div className="py-4 text-center"> <div className="py-4 text-center">
<span className="text-[10px] font-bold text-slate-300"> {total} </span> <span className="text-[10px] font-bold text-slate-300"> {total} </span>
</div> </div>