- FastAPI backend with SQLModel, Alembic migrations, AgentScope agents - Next.js 15 frontend with React 19, Tailwind, Zustand, React Flow - Multi-provider AI system (DashScope, Kling, MiniMax, Volcengine, OpenAI, etc.) - All HTTP clients migrated from sync requests to async httpx - Admin-managed API keys via environment variables - SSRF vulnerability fixed in ensure_url()
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
'use client';
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import {
|
|
UserPlus,
|
|
FolderPlus,
|
|
CheckSquare,
|
|
Zap,
|
|
Settings,
|
|
AlertCircle,
|
|
} from 'lucide-react';
|
|
|
|
interface ActivityItem {
|
|
id: string;
|
|
type: 'user_created' | 'project_created' | 'task_completed' | 'generation_started' | 'system' | 'error';
|
|
title: string;
|
|
description?: string;
|
|
timestamp: string;
|
|
user?: {
|
|
id: string;
|
|
username: string;
|
|
};
|
|
}
|
|
|
|
interface RecentActivityProps {
|
|
activities?: ActivityItem[];
|
|
isLoading: boolean;
|
|
}
|
|
|
|
const activityIcons = {
|
|
user_created: { icon: UserPlus, color: 'text-blue-500', bgColor: 'bg-blue-500/10' },
|
|
project_created: { icon: FolderPlus, color: 'text-emerald-500', bgColor: 'bg-emerald-500/10' },
|
|
task_completed: { icon: CheckSquare, color: 'text-amber-500', bgColor: 'bg-amber-500/10' },
|
|
generation_started: { icon: Zap, color: 'text-purple-500', bgColor: 'bg-purple-500/10' },
|
|
system: { icon: Settings, color: 'text-gray-500', bgColor: 'bg-gray-500/10' },
|
|
error: { icon: AlertCircle, color: 'text-red-500', bgColor: 'bg-red-500/10' },
|
|
};
|
|
|
|
function formatRelativeTime(timestamp: string): string {
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
|
|
if (diffInSeconds < 60) {
|
|
return '刚刚';
|
|
} else if (diffInSeconds < 3600) {
|
|
const minutes = Math.floor(diffInSeconds / 60);
|
|
return `${minutes} 分钟前`;
|
|
} else if (diffInSeconds < 86400) {
|
|
const hours = Math.floor(diffInSeconds / 3600);
|
|
return `${hours} 小时前`;
|
|
} else if (diffInSeconds < 604800) {
|
|
const days = Math.floor(diffInSeconds / 86400);
|
|
return `${days} 天前`;
|
|
} else {
|
|
return date.toLocaleDateString('zh-CN', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
});
|
|
}
|
|
}
|
|
|
|
export function RecentActivity({ activities, isLoading }: RecentActivityProps) {
|
|
return (
|
|
<Card className="h-full">
|
|
<CardHeader>
|
|
<CardTitle>最近活动</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<div className="space-y-4">
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<div key={i} className="flex items-start gap-4">
|
|
<Skeleton className="h-10 w-10 rounded-full" />
|
|
<div className="flex-1 space-y-2">
|
|
<Skeleton className="h-4 w-3/4" />
|
|
<Skeleton className="h-3 w-1/2" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : activities && activities.length > 0 ? (
|
|
<ScrollArea className="h-[400px] pr-4">
|
|
<div className="space-y-4">
|
|
{activities.map((activity) => {
|
|
const { icon: Icon, color, bgColor } = activityIcons[activity.type];
|
|
|
|
return (
|
|
<div key={activity.id} className="flex items-start gap-4">
|
|
<div className={`${bgColor} p-2 rounded-full shrink-0`}>
|
|
<Icon className={`h-4 w-4 ${color}`} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-foreground">
|
|
{activity.title}
|
|
</p>
|
|
{activity.description && (
|
|
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
|
{activity.description}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span className="text-xs text-muted-foreground">
|
|
{formatRelativeTime(activity.timestamp)}
|
|
</span>
|
|
{activity.user && (
|
|
<>
|
|
<span className="text-xs text-muted-foreground">·</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{activity.user.username}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</ScrollArea>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center h-[200px] text-muted-foreground">
|
|
<AlertCircle className="h-8 w-8 mb-2 opacity-50" />
|
|
<p className="text-sm">暂无活动记录</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|