mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-25 22:06:06 +00:00
feat: implement LoadingSpinner component and replace existing loaders across the application
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
||||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||||
|
|
||||||
function SpaceOAuthCallbackContent() {
|
function SpaceOAuthCallbackContent() {
|
||||||
@@ -174,9 +175,7 @@ function SpaceOAuthCallbackContent() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col items-center space-y-4">
|
<CardContent className="flex flex-col items-center space-y-4">
|
||||||
{status === 'loading' && (
|
{status === 'loading' && <LoadingSpinner size="lg" text="" />}
|
||||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
|
||||||
)}
|
|
||||||
{status === 'confirm' && (
|
{status === 'confirm' && (
|
||||||
<>
|
<>
|
||||||
<AlertTriangle className="h-12 w-12 text-yellow-500" />
|
<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">
|
<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">
|
<Card className="w-[400px] shadow-lg dark:shadow-white/10">
|
||||||
<CardContent className="flex flex-col items-center py-12">
|
<CardContent className="flex flex-col items-center py-12">
|
||||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
<LoadingSpinner size="lg" text="" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import styles from './HomeSidebar.module.css';
|
import styles from './HomeSidebar.module.css';
|
||||||
import { useEffect, useState, Suspense } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
SidebarChild,
|
SidebarChild,
|
||||||
SidebarChildVO,
|
SidebarChildVO,
|
||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
Lightbulb,
|
Lightbulb,
|
||||||
LogOut,
|
LogOut,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Loader2,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ function compareVersions(v1: string, v2: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO 侧边导航栏要加动画
|
// TODO 侧边导航栏要加动画
|
||||||
function HomeSidebarContent({
|
export default function HomeSidebar({
|
||||||
onSelectedChangeAction,
|
onSelectedChangeAction,
|
||||||
}: {
|
}: {
|
||||||
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
|
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
|
||||||
@@ -484,25 +483,3 @@ function HomeSidebarContent({
|
|||||||
</div>
|
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 { MessageContentRenderer } from './components/MessageContentRenderer';
|
||||||
import { MessageDetails } from './types/monitoring';
|
import { MessageDetails } from './types/monitoring';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import { LoadingSpinner, LoadingPage } from '@/components/ui/loading-spinner';
|
||||||
|
|
||||||
interface RawMessageData {
|
interface RawMessageData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -262,11 +263,10 @@ function MonitoringPageContent() {
|
|||||||
<TabsContent value="messages" className="p-6 m-0">
|
<TabsContent value="messages" className="p-6 m-0">
|
||||||
<div>
|
<div>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 py-12">
|
<div className="py-12 flex justify-center">
|
||||||
<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>
|
<LoadingSpinner
|
||||||
<p className="text-sm font-medium">
|
text={t('monitoring.messageList.loading')}
|
||||||
{t('monitoring.messageList.loading')}
|
/>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -363,8 +363,8 @@ function MonitoringPageContent() {
|
|||||||
{expandedMessageId === msg.id && (
|
{expandedMessageId === msg.id && (
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-900">
|
<div className="border-t border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-900">
|
||||||
{loadingDetails[msg.id] && (
|
{loadingDetails[msg.id] && (
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 py-4">
|
<div className="py-4 flex justify-center">
|
||||||
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-white"></div>
|
<LoadingSpinner size="sm" text="" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loadingDetails[msg.id] &&
|
{!loadingDetails[msg.id] &&
|
||||||
@@ -410,9 +410,8 @@ function MonitoringPageContent() {
|
|||||||
<TabsContent value="modelCalls" className="p-6 m-0">
|
<TabsContent value="modelCalls" className="p-6 m-0">
|
||||||
<div>
|
<div>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 py-12">
|
<div className="py-12 flex justify-center">
|
||||||
<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>
|
<LoadingSpinner text={t('common.loading')} />
|
||||||
<p className="text-sm font-medium">{t('common.loading')}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -629,9 +628,8 @@ function MonitoringPageContent() {
|
|||||||
<TabsContent value="errors" className="p-6 m-0">
|
<TabsContent value="errors" className="p-6 m-0">
|
||||||
<div>
|
<div>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 py-12">
|
<div className="py-12 flex justify-center">
|
||||||
<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>
|
<LoadingSpinner text={t('common.loading')} />
|
||||||
<p className="text-sm font-medium">{t('common.loading')}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -810,7 +808,7 @@ function MonitoringPageContent() {
|
|||||||
|
|
||||||
export default function MonitoringPage() {
|
export default function MonitoringPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<LoadingPage />}>
|
||||||
<MonitoringPageContent />
|
<MonitoringPageContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { PluginV4 } from '@/app/infra/entities/plugin';
|
|||||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ApiRespMarketplacePlugins } from '@/app/infra/entities/api';
|
import { ApiRespMarketplacePlugins } from '@/app/infra/entities/api';
|
||||||
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
||||||
|
|
||||||
interface SortOption {
|
interface SortOption {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -460,8 +461,7 @@ function MarketPageContent({
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-8 w-8 animate-spin" />
|
<LoadingSpinner text={t('market.loading')} />
|
||||||
<span className="ml-2">{t('market.loading')}</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : plugins.length === 0 ? (
|
) : plugins.length === 0 ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
@@ -484,8 +484,7 @@ function MarketPageContent({
|
|||||||
{/* Loading more indicator */}
|
{/* Loading more indicator */}
|
||||||
{isLoadingMore && (
|
{isLoadingMore && (
|
||||||
<div className="flex items-center justify-center py-6">
|
<div className="flex items-center justify-center py-6">
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
<LoadingSpinner size="sm" text={t('market.loadingMore')} />
|
||||||
<span className="ml-2">{t('market.loadingMore')}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -522,8 +521,7 @@ export default function MarketPage({
|
|||||||
fallback={
|
fallback={
|
||||||
<div className="container mx-auto px-4 py-6">
|
<div className="container mx-auto px-4 py-6">
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-8 w-8 animate-spin" />
|
<LoadingSpinner text="加载中..." />
|
||||||
<span className="ml-2">加载中...</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { toast } from 'sonner';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||||
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
||||||
|
|
||||||
const formSchema = (t: (key: string) => string) =>
|
const formSchema = (t: (key: string) => string) =>
|
||||||
z.object({
|
z.object({
|
||||||
@@ -123,7 +124,7 @@ export default function Login() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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