363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useSetPageInfo } from '@/components/providers/dashboard-provider';
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nice/ui/components/card';
|
||
|
|
import { Badge } from '@nice/ui/components/badge';
|
||
|
|
import { Button } from '@nice/ui/components/button';
|
||
|
|
import { Progress } from '@nice/ui/components/progress';
|
||
|
|
import {
|
||
|
|
IconFileText,
|
||
|
|
IconEye,
|
||
|
|
IconHeart,
|
||
|
|
IconCloud,
|
||
|
|
IconTrendingUp,
|
||
|
|
IconTrendingDown,
|
||
|
|
IconBell,
|
||
|
|
IconCalendar,
|
||
|
|
IconClock,
|
||
|
|
IconEdit,
|
||
|
|
IconShare,
|
||
|
|
IconBookmark,
|
||
|
|
IconChartBar,
|
||
|
|
IconDatabase,
|
||
|
|
IconFolder,
|
||
|
|
IconDownload,
|
||
|
|
IconUpload,
|
||
|
|
} from '@tabler/icons-react';
|
||
|
|
|
||
|
|
// 模拟用户个人数据
|
||
|
|
const userStats = {
|
||
|
|
articles: {
|
||
|
|
total: 24,
|
||
|
|
published: 18,
|
||
|
|
draft: 6,
|
||
|
|
totalViews: 15420,
|
||
|
|
totalLikes: 892,
|
||
|
|
thisMonth: 3,
|
||
|
|
growth: '+12%',
|
||
|
|
},
|
||
|
|
storage: {
|
||
|
|
used: '1.2GB',
|
||
|
|
total: '5GB',
|
||
|
|
usedBytes: 1288490189,
|
||
|
|
totalBytes: 5368709120,
|
||
|
|
filesCount: 156,
|
||
|
|
foldersCount: 12,
|
||
|
|
},
|
||
|
|
engagement: {
|
||
|
|
avgViews: 642,
|
||
|
|
avgLikes: 37,
|
||
|
|
comments: 234,
|
||
|
|
shares: 89,
|
||
|
|
bookmarks: 156,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const recentNotifications = [
|
||
|
|
{ id: 1, type: 'like', message: '您的文章《Next.js 最佳实践》收到了新的点赞', time: '5分钟前', unread: true },
|
||
|
|
{ id: 2, type: 'comment', message: '用户李四评论了您的文章《TypeScript 进阶指南》', time: '1小时前', unread: true },
|
||
|
|
{ id: 3, type: 'follow', message: '用户王五关注了您', time: '2小时前', unread: false },
|
||
|
|
{ id: 4, type: 'system', message: '您的存储空间使用已达到60%', time: '6小时前', unread: false },
|
||
|
|
];
|
||
|
|
|
||
|
|
const recentArticles = [
|
||
|
|
{ id: 1, title: 'Next.js 最佳实践指南', status: 'published', views: 1240, likes: 89, publishedAt: '2024-01-15' },
|
||
|
|
{ id: 2, title: 'TypeScript 进阶技巧', status: 'published', views: 856, likes: 67, publishedAt: '2024-01-12' },
|
||
|
|
{ id: 3, title: 'React Hooks 深度解析', status: 'draft', views: 0, likes: 0, publishedAt: null },
|
||
|
|
{ id: 4, title: 'Tailwind CSS 实战教程', status: 'published', views: 1523, likes: 124, publishedAt: '2024-01-08' },
|
||
|
|
];
|
||
|
|
|
||
|
|
const quickActions = [
|
||
|
|
{ title: '写新文章', icon: IconEdit, href: '/editor', description: '开始创作新的文章' },
|
||
|
|
{ title: '上传文件', icon: IconUpload, href: '/resource', description: '上传文件到云盘' },
|
||
|
|
// { title: '查看统计', icon: IconChartBar, href: '/articles/analytics', description: '查看文章数据分析' },
|
||
|
|
// { title: '管理文件', icon: IconFolder, href: '/drive', description: '管理您的云盘文件' },
|
||
|
|
];
|
||
|
|
|
||
|
|
function StatsCard({
|
||
|
|
title,
|
||
|
|
value,
|
||
|
|
description,
|
||
|
|
icon: Icon,
|
||
|
|
trend,
|
||
|
|
trendValue,
|
||
|
|
className = '',
|
||
|
|
}: {
|
||
|
|
title: string;
|
||
|
|
value: string | number;
|
||
|
|
description: string;
|
||
|
|
icon: any;
|
||
|
|
trend?: 'up' | 'down';
|
||
|
|
trendValue?: string;
|
||
|
|
className?: string;
|
||
|
|
}) {
|
||
|
|
return (
|
||
|
|
<Card className={className}>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">{value}</div>
|
||
|
|
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
|
||
|
|
<span>{description}</span>
|
||
|
|
{trend && trendValue && (
|
||
|
|
<div className="flex items-center">
|
||
|
|
{trend === 'up' ? (
|
||
|
|
<IconTrendingUp className="h-3 w-3 text-green-500" />
|
||
|
|
) : (
|
||
|
|
<IconTrendingDown className="h-3 w-3 text-red-500" />
|
||
|
|
)}
|
||
|
|
<span className={trend === 'up' ? 'text-green-500' : 'text-red-500'}>{trendValue}</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function StorageCard() {
|
||
|
|
const usagePercent = Math.round((userStats.storage.usedBytes / userStats.storage.totalBytes) * 100);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="flex items-center space-x-2">
|
||
|
|
<IconCloud className="h-5 w-5" />
|
||
|
|
<span>存储使用情况</span>
|
||
|
|
</CardTitle>
|
||
|
|
<CardDescription>您的云盘存储空间使用状态</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<span className="text-sm font-medium">已使用空间</span>
|
||
|
|
<span className="text-sm text-muted-foreground">
|
||
|
|
{userStats.storage.used} / {userStats.storage.total}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Progress value={usagePercent} className="h-2" />
|
||
|
|
<div className="flex justify-between text-xs text-muted-foreground">
|
||
|
|
<span>{usagePercent}% 已使用</span>
|
||
|
|
<span>{100 - usagePercent}% 可用</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4 pt-2">
|
||
|
|
<div className="text-center">
|
||
|
|
<div className="text-2xl font-bold text-primary">{userStats.storage.filesCount}</div>
|
||
|
|
<div className="text-xs text-muted-foreground">文件数量</div>
|
||
|
|
</div>
|
||
|
|
<div className="text-center">
|
||
|
|
<div className="text-2xl font-bold text-primary">{userStats.storage.foldersCount}</div>
|
||
|
|
<div className="text-xs text-muted-foreground">文件夹数量</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function NotificationsCard() {
|
||
|
|
const getNotificationIcon = (type: string) => {
|
||
|
|
switch (type) {
|
||
|
|
case 'like':
|
||
|
|
return <IconHeart className="h-4 w-4 text-red-500" />;
|
||
|
|
case 'comment':
|
||
|
|
return <IconFileText className="h-4 w-4 text-blue-500" />;
|
||
|
|
case 'follow':
|
||
|
|
return <IconShare className="h-4 w-4 text-green-500" />;
|
||
|
|
case 'system':
|
||
|
|
return <IconBell className="h-4 w-4 text-orange-500" />;
|
||
|
|
default:
|
||
|
|
return <IconBell className="h-4 w-4" />;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="flex items-center justify-between">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<IconBell className="h-5 w-5" />
|
||
|
|
<span>最新通知</span>
|
||
|
|
</div>
|
||
|
|
<Badge variant="secondary">{recentNotifications.filter((n) => n.unread).length}</Badge>
|
||
|
|
</CardTitle>
|
||
|
|
<CardDescription>您的最新消息和通知</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="space-y-3">
|
||
|
|
{recentNotifications.slice(0, 4).map((notification) => (
|
||
|
|
<div key={notification.id} className="flex items-start space-x-3">
|
||
|
|
<div className="flex-shrink-0 mt-1">{getNotificationIcon(notification.type)}</div>
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<p className={`text-sm ${notification.unread ? 'font-medium' : ''}`}>{notification.message}</p>
|
||
|
|
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
|
||
|
|
<IconClock className="h-3 w-3" />
|
||
|
|
<span>{notification.time}</span>
|
||
|
|
{notification.unread && <div className="w-2 h-2 bg-blue-500 rounded-full ml-2" />}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<div className="mt-4 pt-4 border-t">
|
||
|
|
<Button variant="outline" size="sm" className="w-full">
|
||
|
|
查看所有通知
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function RecentArticlesCard() {
|
||
|
|
const getStatusBadge = (status: string) => {
|
||
|
|
switch (status) {
|
||
|
|
case 'published':
|
||
|
|
return <Badge variant="default">已发布</Badge>;
|
||
|
|
case 'draft':
|
||
|
|
return <Badge variant="secondary">草稿</Badge>;
|
||
|
|
default:
|
||
|
|
return <Badge variant="outline">其他</Badge>;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="flex items-center space-x-2">
|
||
|
|
<IconFileText className="h-5 w-5" />
|
||
|
|
<span>最近文章</span>
|
||
|
|
</CardTitle>
|
||
|
|
<CardDescription>您最近创作的文章</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="space-y-4">
|
||
|
|
{recentArticles.map((article) => (
|
||
|
|
<div key={article.id} className="flex items-center justify-between">
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<p className="text-sm font-medium truncate">{article.title}</p>
|
||
|
|
{getStatusBadge(article.status)}
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center space-x-4 text-xs text-muted-foreground mt-1">
|
||
|
|
<div className="flex items-center space-x-1">
|
||
|
|
<IconEye className="h-3 w-3" />
|
||
|
|
<span>{article.views}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center space-x-1">
|
||
|
|
<IconHeart className="h-3 w-3" />
|
||
|
|
<span>{article.likes}</span>
|
||
|
|
</div>
|
||
|
|
{article.publishedAt && (
|
||
|
|
<div className="flex items-center space-x-1">
|
||
|
|
<IconCalendar className="h-3 w-3" />
|
||
|
|
<span>{article.publishedAt}</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<div className="mt-4 pt-4 border-t">
|
||
|
|
<Button variant="outline" size="sm" className="w-full">
|
||
|
|
查看所有文章
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function QuickActionsCard() {
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>快捷操作</CardTitle>
|
||
|
|
<CardDescription>常用功能快速入口</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="grid grid-cols-2 gap-3">
|
||
|
|
{quickActions.map((action, index) => (
|
||
|
|
<Button
|
||
|
|
key={index}
|
||
|
|
variant="outline"
|
||
|
|
className="h-auto p-3 flex flex-col items-center justify-center space-y-2"
|
||
|
|
asChild
|
||
|
|
>
|
||
|
|
<a href={action.href}>
|
||
|
|
<action.icon className="h-5 w-5" />
|
||
|
|
<div className="text-center">
|
||
|
|
<div className="text-xs font-medium">{action.title}</div>
|
||
|
|
<div className="text-xs text-muted-foreground">{action.description}</div>
|
||
|
|
</div>
|
||
|
|
</a>
|
||
|
|
</Button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function DashboardPage() {
|
||
|
|
// 设置页面信息
|
||
|
|
useSetPageInfo({
|
||
|
|
title: '我的仪表盘',
|
||
|
|
subtitle: '欢迎回来,查看您的创作数据和最新动态',
|
||
|
|
});
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="@container/main flex flex-1 flex-col gap-6 p-6">
|
||
|
|
{/* 统计卡片 */}
|
||
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||
|
|
<StatsCard
|
||
|
|
title="发布文章"
|
||
|
|
value={userStats.articles.published}
|
||
|
|
description={`总共 ${userStats.articles.total} 篇`}
|
||
|
|
icon={IconFileText}
|
||
|
|
trend="up"
|
||
|
|
trendValue={userStats.articles.growth}
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
title="总阅读量"
|
||
|
|
value={userStats.articles.totalViews.toLocaleString()}
|
||
|
|
description={`平均 ${userStats.engagement.avgViews} 次/篇`}
|
||
|
|
icon={IconEye}
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
title="获得点赞"
|
||
|
|
value={userStats.articles.totalLikes.toLocaleString()}
|
||
|
|
description={`平均 ${userStats.engagement.avgLikes} 个/篇`}
|
||
|
|
icon={IconHeart}
|
||
|
|
trend="up"
|
||
|
|
trendValue="+8%"
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
title="存储空间"
|
||
|
|
value={userStats.storage.used}
|
||
|
|
description={`总容量 ${userStats.storage.total}`}
|
||
|
|
icon={IconDatabase}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 详细信息第一行 */}
|
||
|
|
<div className="grid gap-6 md:grid-cols-2">
|
||
|
|
<StorageCard />
|
||
|
|
<NotificationsCard />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 详细信息第二行 */}
|
||
|
|
<div className="grid gap-6 md:grid-cols-2">
|
||
|
|
<RecentArticlesCard />
|
||
|
|
<QuickActionsCard />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|