Files
pixel/frontend/src/components/admin/dashboard/RecentActivity.tsx
张鹏 f9f4560459 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()
2026-04-29 01:20:12 +08:00

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>
);
}