feat(scheduling): replace spinner with skeleton loading placeholders
- Full-page skeleton on initial load: card placeholders + list row placeholders - List skeleton on refresh: 6 rows with pulse animation - Skeleton blocks match actual layout (color bar, plate, badges, info line) - Uses Tailwind animate-pulse for smooth loading effect Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,66 @@ function FilterSelect({ label, options, value, onChange, placeholder }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Skeleton pulse block */
|
||||||
|
function Sk({ className }: { className?: string }) {
|
||||||
|
return <div className={`animate-pulse bg-slate-200/70 rounded ${className ?? ''}`} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SkeletonPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F0F4F8] font-sans p-3 md:p-6">
|
||||||
|
<div className="max-w-6xl mx-auto flex flex-col gap-3 pb-16 md:pb-0">
|
||||||
|
{/* Cards skeleton */}
|
||||||
|
<div className="grid grid-cols-3 gap-2.5">
|
||||||
|
{[0, 1, 2].map(i => (
|
||||||
|
<div key={i} className="p-4 rounded-2xl bg-white border border-slate-100 space-y-2.5">
|
||||||
|
<Sk className="h-3 w-16" />
|
||||||
|
<Sk className="h-7 w-12" />
|
||||||
|
<Sk className="h-2.5 w-24" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List card skeleton */}
|
||||||
|
<div className="bg-white rounded-2xl border border-slate-200/60 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-4 py-3 border-b border-slate-100 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Sk className="h-4 w-28" />
|
||||||
|
<div className="flex gap-2"><Sk className="h-6 w-6 rounded-lg" /><Sk className="h-6 w-6 rounded-lg" /></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[0, 1, 2, 3].map(i => <Sk key={i} className="h-7 w-20 rounded-full" />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rows */}
|
||||||
|
<div className="divide-y divide-slate-50">
|
||||||
|
{Array.from({ length: 8 }).map((_, i) => (
|
||||||
|
<div key={i} className="px-4 py-3 flex items-center gap-3">
|
||||||
|
<Sk className="w-1 h-10 rounded-full" />
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Sk className="h-3.5 w-20" />
|
||||||
|
<Sk className="h-3 w-10 rounded-full" />
|
||||||
|
<Sk className="h-3 w-14" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Sk className="h-2.5 w-28" />
|
||||||
|
<Sk className="h-2.5 w-16" />
|
||||||
|
<Sk className="h-2.5 w-14" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Sk className="h-4 w-8" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function SchedulingModule() {
|
export default function SchedulingModule() {
|
||||||
const [data, setData] = useState<SchedulingResponse | null>(null);
|
const [data, setData] = useState<SchedulingResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -132,6 +192,9 @@ export default function SchedulingModule() {
|
|||||||
const summary = data?.summary;
|
const summary = data?.summary;
|
||||||
const activeFilterCount = [filters.plateSearch, filters.region, filters.vehicleType, filters.customer, filters.department, filters.manager].filter(Boolean).length;
|
const activeFilterCount = [filters.plateSearch, filters.region, filters.vehicleType, filters.customer, filters.department, filters.manager].filter(Boolean).length;
|
||||||
|
|
||||||
|
// Initial load — full page skeleton
|
||||||
|
if (loading && !data) return <SkeletonPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#F0F4F8] text-slate-800 font-sans p-3 md:p-6" style={{ overflowX: 'clip' }}>
|
<div className="min-h-screen bg-[#F0F4F8] text-slate-800 font-sans p-3 md:p-6" style={{ overflowX: 'clip' }}>
|
||||||
<div className="max-w-6xl mx-auto flex flex-col gap-3 pb-16 md:pb-0">
|
<div className="max-w-6xl mx-auto flex flex-col gap-3 pb-16 md:pb-0">
|
||||||
@@ -305,9 +368,27 @@ export default function SchedulingModule() {
|
|||||||
<div className="px-4 py-1.5 border-b border-slate-50 text-[10px] text-slate-400">共 {filteredSuggestions.length} 条结果</div>
|
<div className="px-4 py-1.5 border-b border-slate-50 text-[10px] text-slate-400">共 {filteredSuggestions.length} 条结果</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading && !data ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center py-20">
|
/* List skeleton while refreshing */
|
||||||
<div className="w-6 h-6 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
|
<div className="divide-y divide-slate-50">
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<div key={i} className="px-4 py-3 flex items-center gap-3">
|
||||||
|
<Sk className="w-1 h-10 rounded-full" />
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Sk className="h-3.5 w-20" />
|
||||||
|
<Sk className="h-3 w-10 rounded-full" />
|
||||||
|
<Sk className="h-3 w-14" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Sk className="h-2.5 w-28" />
|
||||||
|
<Sk className="h-2.5 w-16" />
|
||||||
|
<Sk className="h-2.5 w-14" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Sk className="h-4 w-8" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<SuggestionList suggestions={filteredSuggestions} onSelect={setSelectedSuggestion} />
|
<SuggestionList suggestions={filteredSuggestions} onSelect={setSelectedSuggestion} />
|
||||||
|
|||||||
Reference in New Issue
Block a user