fix: show mileage query loading animation and bump version to 1.1.15
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ln-bi",
|
||||
"version": "1.1.14",
|
||||
"version": "1.1.15",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ln-bi",
|
||||
"version": "1.1.14",
|
||||
"version": "1.1.15",
|
||||
"dependencies": {
|
||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||
"@hono/node-server": "^1.13.0",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ln-bi",
|
||||
"private": true,
|
||||
"version": "1.1.14",
|
||||
"version": "1.1.15",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
import { fetchMonitoring } from './api';
|
||||
import { exportMileageXlsx } from './xlsx-export';
|
||||
import VehicleDetailModal from './VehicleDetailModal';
|
||||
import MileageLoadingOverlay from './MileageLoadingOverlay';
|
||||
import {
|
||||
defaultMileageDate,
|
||||
getRangePreset,
|
||||
@@ -148,10 +149,14 @@ export default function MonitoringView() {
|
||||
return inAlertTarget && Math.max(0, v.dailyKm || 0) >= HIGH_MILEAGE_ALERT_KM;
|
||||
}, [filterTargetNames]);
|
||||
|
||||
const queryRequestRef = useRef(0);
|
||||
|
||||
// 加载首页数据
|
||||
const loadFirstPage = useCallback((showPageLoading = true) => {
|
||||
const requestId = ++queryRequestRef.current;
|
||||
if (showPageLoading) setPageLoading(true);
|
||||
return fetchMonitoring(buildInitialMonitoringQuery(monitoringQuery, PAGE_SIZE)).then(d => {
|
||||
if (requestId !== queryRequestRef.current) return;
|
||||
setVehicles(d.vehicles);
|
||||
setStats(d.stats);
|
||||
setFilterOptions(d.filters);
|
||||
@@ -162,31 +167,33 @@ export default function MonitoringView() {
|
||||
setHasMore(d.page < d.totalPages);
|
||||
setLastRefreshedAt(new Date());
|
||||
}).catch(() => {}).finally(() => {
|
||||
if (showPageLoading) setPageLoading(false);
|
||||
if (requestId === queryRequestRef.current) setPageLoading(false);
|
||||
});
|
||||
}, [monitoringQuery, rangeStart, rangeEnd]);
|
||||
|
||||
const handleManualRefresh = useCallback(async () => {
|
||||
if (manualRefreshing) return;
|
||||
if (manualRefreshing || pageLoading) return;
|
||||
setManualRefreshing(true);
|
||||
try {
|
||||
await loadFirstPage(false);
|
||||
} finally {
|
||||
setManualRefreshing(false);
|
||||
}
|
||||
}, [loadFirstPage, manualRefreshing]);
|
||||
}, [loadFirstPage, manualRefreshing, pageLoading]);
|
||||
|
||||
// 加载更多
|
||||
const loadMore = useCallback(() => {
|
||||
if (loadingMore || !hasMore) return;
|
||||
if (pageLoading || loadingMore || !hasMore) return;
|
||||
const requestId = queryRequestRef.current;
|
||||
const nextPage = page + 1;
|
||||
setLoadingMore(true);
|
||||
fetchMonitoring(buildNextPageMonitoringQuery(monitoringQuery, PAGE_SIZE, nextPage)).then(d => {
|
||||
if (requestId !== queryRequestRef.current) return;
|
||||
setVehicles(prev => [...prev, ...d.vehicles]);
|
||||
setPage(nextPage);
|
||||
setHasMore(nextPage < d.totalPages);
|
||||
}).catch(() => {}).finally(() => setLoadingMore(false));
|
||||
}, [monitoringQuery, page, loadingMore, hasMore]);
|
||||
}, [monitoringQuery, page, pageLoading, loadingMore, hasMore]);
|
||||
|
||||
// 筛选/排序变化时重新加载
|
||||
useEffect(() => {
|
||||
@@ -302,6 +309,9 @@ export default function MonitoringView() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{pageLoading && <MileageLoadingOverlay />}
|
||||
</AnimatePresence>
|
||||
{/* 顶部哨兵:离开视口时显示回到顶部按钮 */}
|
||||
<div ref={topSentinelRef} className="h-0" />
|
||||
|
||||
|
||||
@@ -1,45 +1,6 @@
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
import MileageLoadingOverlay from '../MileageLoadingOverlay';
|
||||
import { fmtDate } from './utils';
|
||||
|
||||
export default function DailyReportLoadingOverlay({ reportDate }: { reportDate: 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">正在加载日报</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>
|
||||
);
|
||||
return <MileageLoadingOverlay title="正在加载日报" description={`正在汇总 ${fmtDate(reportDate)} 的里程数据`} />;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function VehicleList({
|
||||
})}
|
||||
</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">
|
||||
<p className="text-xs font-bold text-slate-300">未找到匹配数据</p>
|
||||
</div>
|
||||
@@ -146,7 +146,7 @@ export default function VehicleList({
|
||||
<span className="text-[10px] font-bold text-slate-400">加载中...</span>
|
||||
</div>
|
||||
)}
|
||||
{!hasMore && vehicles.length > 0 && (
|
||||
{!pageLoading && !hasMore && vehicles.length > 0 && (
|
||||
<div className="py-4 text-center">
|
||||
<span className="text-[10px] font-bold text-slate-300">已加载全部 {total} 条</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user