Initial commit: Pixel AI comic/video creation platform
- 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()
This commit is contained in:
132
frontend/src/components/admin/dashboard/RecentActivity.tsx
Normal file
132
frontend/src/components/admin/dashboard/RecentActivity.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user