mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat: implement LoadingSpinner component and replace existing loaders across the application
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
5
web/src/app/home/loading.tsx
Normal file
5
web/src/app/home/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { LoadingPage } from '@/components/ui/loading-spinner';
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
83
web/src/components/ui/loading-spinner.tsx
Normal file
83
web/src/components/ui/loading-spinner.tsx
Normal 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} />;
|
||||
}
|
||||
Reference in New Issue
Block a user