feat: implement LoadingSpinner component and replace existing loaders across the application

This commit is contained in:
Junyan Qin
2026-01-29 15:24:23 +08:00
parent 13f42857f5
commit b89a240250
7 changed files with 111 additions and 50 deletions

View File

@@ -19,6 +19,7 @@ import {
CardDescription,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
import langbotIcon from '@/app/assets/langbot-logo.webp';
function SpaceOAuthCallbackContent() {
@@ -174,9 +175,7 @@ function SpaceOAuthCallbackContent() {
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center space-y-4">
{status === 'loading' && (
<Loader2 className="h-12 w-12 animate-spin text-primary" />
)}
{status === 'loading' && <LoadingSpinner size="lg" text="" />}
{status === 'confirm' && (
<>
<AlertTriangle className="h-12 w-12 text-yellow-500" />
@@ -232,7 +231,7 @@ function LoadingFallback() {
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
<Card className="w-[400px] shadow-lg dark:shadow-white/10">
<CardContent className="flex flex-col items-center py-12">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
<LoadingSpinner size="lg" text="" />
</CardContent>
</Card>
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import styles from './HomeSidebar.module.css';
import { useEffect, useState, Suspense } from 'react';
import { useEffect, useState } from 'react';
import {
SidebarChild,
SidebarChildVO,
@@ -20,7 +20,6 @@ import {
Lightbulb,
LogOut,
KeyRound,
Loader2,
} from 'lucide-react';
import { useTheme } from 'next-themes';
@@ -59,7 +58,7 @@ function compareVersions(v1: string, v2: string): boolean {
}
// TODO 侧边导航栏要加动画
function HomeSidebarContent({
export default function HomeSidebar({
onSelectedChangeAction,
}: {
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
@@ -484,25 +483,3 @@ function HomeSidebarContent({
</div>
);
}
function SidebarLoadingFallback() {
return (
<div className={`${styles.sidebarContainer}`}>
<div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
</div>
);
}
export default function HomeSidebar({
onSelectedChangeAction,
}: {
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
}) {
return (
<Suspense fallback={<SidebarLoadingFallback />}>
<HomeSidebarContent onSelectedChangeAction={onSelectedChangeAction} />
</Suspense>
);
}

View File

@@ -0,0 +1,5 @@
import { LoadingPage } from '@/components/ui/loading-spinner';
export default function Loading() {
return <LoadingPage />;
}

View File

@@ -13,6 +13,7 @@ import { MessageDetailsCard } from './components/MessageDetailsCard';
import { MessageContentRenderer } from './components/MessageContentRenderer';
import { MessageDetails } from './types/monitoring';
import { httpClient } from '@/app/infra/http/HttpClient';
import { LoadingSpinner, LoadingPage } from '@/components/ui/loading-spinner';
interface RawMessageData {
id: string;
@@ -262,11 +263,10 @@ function MonitoringPageContent() {
<TabsContent value="messages" className="p-6 m-0">
<div>
{loading && (
<div className="text-center text-gray-500 dark:text-gray-400 py-12">
<div className="inline-block animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 dark:border-blue-400 mb-4"></div>
<p className="text-sm font-medium">
{t('monitoring.messageList.loading')}
</p>
<div className="py-12 flex justify-center">
<LoadingSpinner
text={t('monitoring.messageList.loading')}
/>
</div>
)}
@@ -363,8 +363,8 @@ function MonitoringPageContent() {
{expandedMessageId === msg.id && (
<div className="border-t border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-900">
{loadingDetails[msg.id] && (
<div className="text-center text-gray-500 dark:text-gray-400 py-4">
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-white"></div>
<div className="py-4 flex justify-center">
<LoadingSpinner size="sm" text="" />
</div>
)}
{!loadingDetails[msg.id] &&
@@ -410,9 +410,8 @@ function MonitoringPageContent() {
<TabsContent value="modelCalls" className="p-6 m-0">
<div>
{loading && (
<div className="text-center text-gray-500 dark:text-gray-400 py-12">
<div className="inline-block animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 dark:border-blue-400 mb-4"></div>
<p className="text-sm font-medium">{t('common.loading')}</p>
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
)}
@@ -629,9 +628,8 @@ function MonitoringPageContent() {
<TabsContent value="errors" className="p-6 m-0">
<div>
{loading && (
<div className="text-center text-gray-500 dark:text-gray-400 py-12">
<div className="inline-block animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 dark:border-blue-400 mb-4"></div>
<p className="text-sm font-medium">{t('common.loading')}</p>
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
)}
@@ -810,7 +808,7 @@ function MonitoringPageContent() {
export default function MonitoringPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Suspense fallback={<LoadingPage />}>
<MonitoringPageContent />
</Suspense>
);

View File

@@ -27,6 +27,7 @@ import { PluginV4 } from '@/app/infra/entities/plugin';
import { extractI18nObject } from '@/i18n/I18nProvider';
import { toast } from 'sonner';
import { ApiRespMarketplacePlugins } from '@/app/infra/entities/api';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
interface SortOption {
value: string;
@@ -460,8 +461,7 @@ function MarketPageContent({
>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">{t('market.loading')}</span>
<LoadingSpinner text={t('market.loading')} />
</div>
) : plugins.length === 0 ? (
<div className="flex items-center justify-center py-12">
@@ -484,8 +484,7 @@ function MarketPageContent({
{/* Loading more indicator */}
{isLoadingMore && (
<div className="flex items-center justify-center py-6">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="ml-2">{t('market.loadingMore')}</span>
<LoadingSpinner size="sm" text={t('market.loadingMore')} />
</div>
)}
@@ -522,8 +521,7 @@ export default function MarketPage({
fallback={
<div className="container mx-auto px-4 py-6">
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">...</span>
<LoadingSpinner text="加载中..." />
</div>
</div>
}

View File

@@ -29,6 +29,7 @@ import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import { ThemeToggle } from '@/components/ui/theme-toggle';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
const formSchema = (t: (key: string) => string) =>
z.object({
@@ -123,7 +124,7 @@ export default function Login() {
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<LoadingSpinner />
</div>
);
}

View File

@@ -0,0 +1,83 @@
import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
interface LoadingSpinnerProps {
/**
* Size variant of the spinner
* @default 'default'
*/
size?: 'sm' | 'default' | 'lg';
/**
* Additional CSS classes
*/
className?: string;
/**
* Loading text to display below the spinner
*/
text?: string;
/**
* Whether to display as full page overlay
* @default false
*/
fullPage?: boolean;
}
const sizeMap = {
sm: 'h-4 w-4',
default: 'h-8 w-8',
lg: 'h-12 w-12',
};
const textSizeMap = {
sm: 'text-xs',
default: 'text-sm',
lg: 'text-base',
};
export function LoadingSpinner({
size = 'default',
className,
text = '加载中...',
fullPage = false,
}: LoadingSpinnerProps) {
const spinner = (
<div className="flex flex-col items-center gap-4">
<Loader2
className={cn('animate-spin text-primary', sizeMap[size], className)}
/>
{text && (
<p className={cn('text-muted-foreground', textSizeMap[size])}>{text}</p>
)}
</div>
);
if (fullPage) {
return (
<div className="fixed inset-0 flex items-center justify-center bg-background">
{spinner}
</div>
);
}
return spinner;
}
/**
* Full page loading component for use in page.tsx or layout.tsx
*/
export function LoadingPage({ text }: { text?: string }) {
return <LoadingSpinner fullPage text={text} />;
}
/**
* Inline loading component for use within components
*/
export function LoadingInline({
size,
text,
}: {
size?: 'sm' | 'default' | 'lg';
text?: string;
}) {
return <LoadingSpinner size={size} text={text} />;
}