feat: 实时监控加载动画 - KPI骨架屏+车辆列表skeleton
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,7 @@ export default function MonitoringView() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [pageLoading, setPageLoading] = useState(true);
|
||||
const [showBackToTop, setShowBackToTop] = useState(false);
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
@@ -131,6 +132,7 @@ export default function MonitoringView() {
|
||||
|
||||
// 加载首页数据
|
||||
const loadFirstPage = useCallback(() => {
|
||||
setPageLoading(true);
|
||||
fetchMonitoring({
|
||||
sortBy,
|
||||
sortOrder,
|
||||
@@ -155,7 +157,7 @@ export default function MonitoringView() {
|
||||
setTotal(d.total);
|
||||
setPage(1);
|
||||
setHasMore(d.page < d.totalPages);
|
||||
}).catch(() => {});
|
||||
}).catch(() => {}).finally(() => setPageLoading(false));
|
||||
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetName, filterPlate, appliedMileageRange, filterDate]);
|
||||
|
||||
// 加载更多
|
||||
@@ -760,22 +762,21 @@ export default function MonitoringView() {
|
||||
|
||||
{/* Sticky header: KPI + 清单标题 */}
|
||||
<div className="sticky top-[44px] z-20 bg-[#F8F9FB] pt-1 pb-1 space-y-2">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<div className={`grid grid-cols-4 gap-2 transition-opacity ${pageLoading ? 'opacity-60' : ''}`}>
|
||||
<div className="col-span-2 bg-slate-900 p-2.5 rounded-xl text-white relative overflow-hidden">
|
||||
<div className="text-[7px] font-bold text-slate-500 uppercase tracking-wider">{sortBy === 'today' ? '今日' : '累计'}总里程</div>
|
||||
<div className="text-lg font-black tracking-tighter leading-tight flex items-baseline gap-1">
|
||||
{Math.round(sortBy === 'today' ? stats.totalToday : stats.totalAll).toLocaleString()}
|
||||
<span className="text-[8px] text-slate-400">km</span>
|
||||
{pageLoading ? <div className="h-5 w-20 bg-slate-700 rounded animate-pulse"></div> : <>{Math.round(sortBy === 'today' ? stats.totalToday : stats.totalAll).toLocaleString()} <span className="text-[8px] text-slate-400">km</span></>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-2.5 rounded-xl border border-gray-100 shadow-sm">
|
||||
<div className="text-[7px] font-bold text-slate-400 uppercase">平均单车</div>
|
||||
<div className="text-sm font-black text-slate-800 leading-tight">{(stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)}</div>
|
||||
<div className="text-sm font-black text-slate-800 leading-tight">{pageLoading ? <div className="h-4 w-8 bg-slate-100 rounded animate-pulse"></div> : (stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)}</div>
|
||||
<div className="text-[7px] text-slate-400">km/台</div>
|
||||
</div>
|
||||
<div className="bg-white p-2.5 rounded-xl border border-gray-100 shadow-sm">
|
||||
<div className="text-[7px] font-bold text-slate-400 uppercase">监控台数</div>
|
||||
<div className="text-sm font-black text-slate-800 leading-tight">{stats.vehicleCount}</div>
|
||||
<div className="text-sm font-black text-slate-800 leading-tight">{pageLoading ? <div className="h-4 w-8 bg-slate-100 rounded animate-pulse"></div> : stats.vehicleCount}</div>
|
||||
<div className="text-[7px] text-slate-400">台</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -788,6 +789,26 @@ export default function MonitoringView() {
|
||||
{/* Vehicle List */}
|
||||
<div className="space-y-1.5">
|
||||
|
||||
{pageLoading && (
|
||||
<div className="space-y-1.5">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="bg-white px-3 py-3 rounded-xl border border-slate-50 shadow-sm flex items-center justify-between animate-pulse">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<div className="w-8 h-8 rounded-lg bg-slate-100"></div>
|
||||
<div className="space-y-1.5 flex-1">
|
||||
<div className="h-3 bg-slate-100 rounded w-24"></div>
|
||||
<div className="h-2 bg-slate-50 rounded w-36"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 text-right">
|
||||
<div className="h-4 bg-slate-100 rounded w-16 ml-auto"></div>
|
||||
<div className="h-2 bg-slate-50 rounded w-20 ml-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-1.5">
|
||||
{filteredVehicles.map((v) => (
|
||||
<motion.div
|
||||
|
||||
Reference in New Issue
Block a user