Files
frontend/src/app/dashboard/page.tsx

260 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/app/dashboard/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import Link from 'next/link';
import Canvas2DSkinPreview from '@/components/skins/Canvas2DSkinPreview';
import { TextureInfo, TextureType, getUserTextures, getUserFavorites, addFavorite, removeFavorite, checkFavoriteStatus } from '@/lib/api/skins';
export default function Dashboard() {
// 由于这是客户端组件我们不能在这里使用getServerSession
// 在实际应用中你应该使用useSession钩子或在服务器端获取会话
// 这里我们模拟一个已登录的状态
const session = { user: { id: 'test_user_1', name: '测试玩家', email: 'test@test.com' } };
// 安全地获取用户ID
const userId = session?.user?.id || 'unknown';
// 状态管理
const [searchTerm, setSearchTerm] = useState('');
const [activeCategory, setActiveCategory] = useState('all');
const [skins, setSkins] = useState<TextureInfo[]>([]);
const [favorites, setFavorites] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 加载用户皮肤
useEffect(() => {
loadUserSkins();
}, []);
const loadUserSkins = async () => {
setLoading(true);
setError(null);
try {
// 获取用户皮肤列表
const response = await getUserTextures({ type: TextureType.SKIN, page: 1, pageSize: 20 });
setSkins(response.textures);
// 获取用户收藏列表
const favResponse = await getUserFavorites({ page: 1, pageSize: 20 });
setFavorites(favResponse.textures);
} catch (err) {
console.error('加载皮肤失败:', err);
setError('无法加载皮肤,请稍后重试');
} finally {
setLoading(false);
}
};
const handleFavoriteToggle = async (textureId: number) => {
try {
// 检查当前收藏状态
const isCurrentlyFavorite = await checkFavoriteStatus(textureId);
if (isCurrentlyFavorite) {
// 取消收藏
await removeFavorite(textureId);
// 更新本地状态
setFavorites(favorites.filter(fav => fav.texture.id !== textureId));
} else {
// 添加收藏
await addFavorite(textureId);
// 获取更新后的收藏列表
const favResponse = await getUserFavorites({ page: 1, pageSize: 20 });
setFavorites(favResponse.textures);
}
} catch (err) {
console.error('操作收藏失败:', err);
alert('操作失败,请稍后重试');
}
};
// 根据搜索词和分类过滤皮肤
const filteredSkins = skins.filter(skin =>
skin.id.toString().includes(searchTerm.toLowerCase())
);
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800 relative overflow-hidden text-gray-900 dark:text-gray-100">
{/* 背景装饰元素 - 渐变模糊效果 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-20 -right-20 w-80 h-80 bg-emerald-400/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-20 -left-20 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl"></div>
<div className="absolute top-1/3 left-1/4 w-64 h-64 bg-emerald-300/10 rounded-full blur-3xl"></div>
</div>
{/* 主要内容区域 */}
<main className="container mx-auto px-4 py-12 relative z-10">
{/* 页面标题和操作按钮 */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-10 gap-4">
<div>
<h1 className="text-3xl font-bold mb-2 text-gray-800 dark:text-white"></h1>
<p className="text-gray-600 dark:text-gray-400">Minecraft皮肤</p>
</div>
<Button asChild className="bg-emerald-600 hover:bg-emerald-700 text-white transition-all duration-300 transform hover:-translate-y-1 shadow-md hover:shadow-lg">
<Link href="/skins/upload">
<span className="mr-2">📤</span>
</Link>
</Button>
</div>
{/* 搜索栏 */}
<div className="mb-8 max-w-4xl mx-auto">
<div className="relative">
<Input
type="text"
placeholder="搜索你的皮肤..."
className="pl-10 pr-4 py-2 w-full border-emerald-200 dark:border-emerald-900/30 focus:border-emerald-500 dark:focus:border-emerald-400 rounded-xl shadow-sm transition-all bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400">
🔍
</div>
</div>
</div>
{/* 皮肤分类标签 */}
<div className="flex overflow-x-auto gap-3 pb-4 mb-8 scrollbar-hide max-w-4xl mx-auto">
<Button
variant={activeCategory === 'all' ? 'default' : 'ghost'}
className={`rounded-full ${activeCategory === 'all' ? 'bg-emerald-600 hover:bg-emerald-700 text-white shadow-md' : 'hover:text-emerald-600 dark:hover:text-emerald-400'} transition-all`}
onClick={() => setActiveCategory('all')}
>
</Button>
<Button
variant={activeCategory === 'recent' ? 'default' : 'ghost'}
className={`rounded-full ${activeCategory === 'recent' ? 'bg-emerald-600 hover:bg-emerald-700 text-white shadow-md' : 'hover:text-emerald-600 dark:hover:text-emerald-400'} transition-all`}
onClick={() => setActiveCategory('recent')}
>
</Button>
<Button
variant={activeCategory === 'popular' ? 'default' : 'ghost'}
className={`rounded-full ${activeCategory === 'popular' ? 'bg-emerald-600 hover:bg-emerald-700 text-white shadow-md' : 'hover:text-emerald-600 dark:hover:text-emerald-400'} transition-all`}
onClick={() => setActiveCategory('popular')}
>
</Button>
<Button
variant={activeCategory === 'favorites' ? 'default' : 'ghost'}
className={`rounded-full ${activeCategory === 'favorites' ? 'bg-emerald-600 hover:bg-emerald-700 text-white shadow-md' : 'hover:text-emerald-600 dark:hover:text-emerald-400'} transition-all`}
onClick={() => setActiveCategory('favorites')}
>
</Button>
</div>
{/* 皮肤网格 */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 max-w-6xl mx-auto">
{loading ? (
// 加载状态
Array.from({ length: 6 }).map((_, index) => (
<Card key={index} className="overflow-hidden border border-emerald-200 dark:border-emerald-900/30 rounded-2xl hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm">
<div className="aspect-square bg-gray-200 dark:bg-gray-700 animate-pulse"></div>
<CardContent className="p-5">
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded w-2/3 mb-2 animate-pulse"></div>
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-1/2 mb-4 animate-pulse"></div>
<div className="flex gap-2">
<div className="h-8 bg-gray-300 dark:bg-gray-600 rounded w-1/3 animate-pulse"></div>
<div className="h-8 bg-gray-300 dark:bg-gray-600 rounded w-1/3 animate-pulse"></div>
</div>
</CardContent>
</Card>
))
) : error ? (
// 错误状态
<div className="col-span-full text-center py-12">
<p className="text-red-500 mb-4">{error}</p>
<Button onClick={loadUserSkins}></Button>
</div>
) : filteredSkins.length === 0 ? (
// 空状态
<div className="col-span-full text-center py-12">
<p className="text-gray-500 dark:text-gray-400 mb-4"></p>
<Button asChild>
<Link href="/skins/upload"></Link>
</Button>
</div>
) : (
// 正常显示皮肤列表
filteredSkins.map((skin) => (
<Card key={skin.id} className="overflow-hidden border border-emerald-200 dark:border-emerald-900/30 rounded-2xl hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm">
<div className="aspect-square bg-gray-100/95 dark:bg-gray-900/95 flex items-center justify-center p-4 relative overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-emerald-50/50 to-teal-50/50 dark:from-emerald-900/10 dark:to-teal-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<Canvas2DSkinPreview
skinUrl={skin.url || `/test-skin.png`}
size={128}
className="max-w-full max-h-full relative z-10 transition-transform duration-500 group-hover:scale-110"
/>
</div>
<CardContent className="p-5">
<div className="flex justify-between items-start mb-2">
<h3 className="font-semibold text-lg text-gray-800 dark:text-white truncate group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors"> #{skin.id}</h3>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-gray-500 hover:text-rose-500 dark:hover:text-rose-400 rounded-full"
onClick={(e) => {
e.stopPropagation();
handleFavoriteToggle(skin.id);
}}
>
</Button>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4"> {new Date(skin.createdAt).toLocaleDateString()}</p>
<div className="flex gap-2">
<Button size="sm" variant="ghost" className="text-xs flex-1 border border-emerald-200 dark:border-emerald-900/30 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-lg">
</Button>
<Button size="sm" variant="ghost" className="text-xs flex-1 border border-emerald-200 dark:border-emerald-900/30 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-lg">
</Button>
<Button size="sm" variant="ghost" className="text-xs flex-1 border border-emerald-200 dark:border-emerald-900/30 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-lg">
</Button>
</div>
</CardContent>
</Card>
))
)}
</div>
{/* 空状态 */}
{filteredSkins.length === 0 && (
<div className="text-center py-16 max-w-4xl mx-auto">
<Card className="border border-emerald-200 dark:border-emerald-900/30 rounded-2xl overflow-hidden bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm shadow-xl">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardContent className="p-8">
<div className="text-6xl mb-4">📦</div>
<h3 className="text-xl font-semibold mb-2 text-gray-800 dark:text-white"></h3>
<p className="text-gray-600 dark:text-gray-400 mb-6 max-w-md mx-auto">
Minecraft皮肤收藏之旅
</p>
<Button asChild className="bg-emerald-600 hover:bg-emerald-700 text-white transition-all duration-300 transform hover:-translate-y-1 shadow-md hover:shadow-lg">
<Link href="/skins/upload">
<span className="mr-2">📤</span>
</Link>
</Button>
</CardContent>
</Card>
</div>
)}
</main>
{/* 页脚 */}
<footer className="bg-gray-900/80 backdrop-blur-md text-gray-300 py-8 mt-16 border-t border-gray-800">
<div className="container mx-auto px-4 text-center relative z-10">
<p className="mb-2"> - Minecraft皮肤分享平台</p>
<p className="text-sm text-gray-400">© {new Date().getFullYear()} </p>
</div>
</footer>
</div>
);
}