Files
carrotskin/src/app/profile/page.tsx

1360 lines
66 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
UserCircleIcon,
PencilIcon,
TrashIcon,
PlusIcon,
EyeIcon,
ArrowDownTrayIcon,
Cog6ToothIcon,
UserIcon,
PhotoIcon,
KeyIcon,
EnvelopeIcon,
HeartIcon,
ArrowLeftOnRectangleIcon,
CloudArrowUpIcon,
XMarkIcon,
ArrowPathIcon,
XCircleIcon
} from '@heroicons/react/24/outline';
import { useAuth } from '@/contexts/AuthContext';
import {
getMyTextures,
getFavoriteTextures,
toggleFavorite,
getProfiles,
createProfile,
updateProfile,
deleteProfile,
setActiveProfile,
getUserProfile,
updateUserProfile,
uploadTexture,
type Texture,
type Profile
} from '@/lib/api';
interface UserProfile {
id: number;
username: string;
email: string;
avatar?: string;
points: number;
role: string;
status: number;
last_login_at?: string;
created_at: string;
updated_at: string;
}
export default function ProfilePage() {
const [activeTab, setActiveTab] = useState<'characters' | 'skins' | 'favorites' | 'settings'>('characters');
const [profiles, setProfiles] = useState<Profile[]>([]);
const [mySkins, setMySkins] = useState<Texture[]>([]);
const [favoriteSkins, setFavoriteSkins] = useState<Texture[]>([]);
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isUploading, setIsUploading] = useState(false);
const [showCreateCharacter, setShowCreateCharacter] = useState(false);
const [showUploadSkin, setShowUploadSkin] = useState(false);
const [newCharacterName, setNewCharacterName] = useState('');
const [newSkinData, setNewSkinData] = useState({
name: '',
description: '',
type: 'SKIN' as 'SKIN' | 'CAPE',
is_public: false,
is_slim: false
});
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [editingProfile, setEditingProfile] = useState<string | null>(null);
const [editProfileName, setEditProfileName] = useState('');
const [uploadProgress, setUploadProgress] = useState(0);
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
const [avatarUploadProgress, setAvatarUploadProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const { user, isAuthenticated, logout } = useAuth();
// 加载用户数据
useEffect(() => {
if (isAuthenticated) {
loadUserData();
}
}, [isAuthenticated]);
const loadUserData = async () => {
setIsLoading(true);
setError(null);
try {
// 加载用户信息
const userResponse = await getUserProfile();
if (userResponse.code === 200) {
setUserProfile(userResponse.data);
} else {
throw new Error(userResponse.message || '获取用户信息失败');
}
// 加载用户档案
const profilesResponse = await getProfiles();
if (profilesResponse.code === 200) {
setProfiles(profilesResponse.data);
} else {
throw new Error(profilesResponse.message || '获取角色列表失败');
}
// 加载用户皮肤
const mySkinsResponse = await getMyTextures({ page: 1, page_size: 50 });
if (mySkinsResponse.code === 200) {
setMySkins(mySkinsResponse.data.list || []);
} else {
throw new Error(mySkinsResponse.message || '获取皮肤列表失败');
}
// 加载收藏的皮肤
const favoritesResponse = await getFavoriteTextures({ page: 1, page_size: 50 });
if (favoritesResponse.code === 200) {
setFavoriteSkins(favoritesResponse.data.list || []);
} else {
throw new Error(favoritesResponse.message || '获取收藏列表失败');
}
} catch (error) {
console.error('加载用户数据失败:', error);
setError(error instanceof Error ? error.message : '加载数据失败,请稍后重试');
} finally {
setIsLoading(false);
}
};
const handleCreateCharacter = async () => {
if (!newCharacterName.trim()) {
alert('请输入角色名称');
return;
}
try {
const response = await createProfile(newCharacterName.trim());
if (response.code === 200) {
setProfiles(prev => [...prev, response.data]);
setNewCharacterName('');
setShowCreateCharacter(false);
alert('角色创建成功!');
} else {
throw new Error(response.message || '创建角色失败');
}
} catch (error) {
console.error('创建角色失败:', error);
alert(error instanceof Error ? error.message : '创建角色失败,请稍后重试');
}
};
const handleDeleteCharacter = async (uuid: string) => {
const character = profiles.find(p => p.uuid === uuid);
if (!character) return;
if (!confirm(`确定要删除角色 "${character.name}" 吗?此操作不可恢复。`)) return;
try {
const response = await deleteProfile(uuid);
if (response.code === 200) {
setProfiles(prev => prev.filter(profile => profile.uuid !== uuid));
alert('角色删除成功!');
} else {
throw new Error(response.message || '删除角色失败');
}
} catch (error) {
console.error('删除角色失败:', error);
alert(error instanceof Error ? error.message : '删除角色失败,请稍后重试');
}
};
const handleSetActiveCharacter = async (uuid: string) => {
try {
const response = await setActiveProfile(uuid);
if (response.code === 200) {
setProfiles(prev => prev.map(profile => ({
...profile,
is_active: profile.uuid === uuid
})));
alert('角色切换成功!');
} else {
throw new Error(response.message || '设置活跃角色失败');
}
} catch (error) {
console.error('设置活跃角色失败:', error);
alert(error instanceof Error ? error.message : '设置活跃角色失败,请稍后重试');
}
};
const handleEditCharacter = async (uuid: string) => {
if (!editProfileName.trim()) {
alert('请输入角色名称');
return;
}
try {
const response = await updateProfile(uuid, { name: editProfileName.trim() });
if (response.code === 200) {
setProfiles(prev => prev.map(profile =>
profile.uuid === uuid ? response.data : profile
));
setEditingProfile(null);
setEditProfileName('');
alert('角色编辑成功!');
} else {
throw new Error(response.message || '编辑角色失败');
}
} catch (error) {
console.error('编辑角色失败:', error);
alert(error instanceof Error ? error.message : '编辑角色失败,请稍后重试');
}
};
const handleDeleteSkin = async (skinId: number) => {
if (!confirm('确定要删除这个皮肤吗?')) return;
setMySkins(prev => prev.filter(skin => skin.id !== skinId));
};
const handleToggleSkinVisibility = async (skinId: number) => {
try {
const skin = mySkins.find(s => s.id === skinId);
if (!skin) return;
// TODO: 添加更新皮肤API调用
setMySkins(prev => prev.map(skin =>
skin.id === skinId ? { ...skin, is_public: !skin.is_public } : skin
));
} catch (error) {
console.error('切换皮肤可见性失败:', error);
}
};
const handleToggleFavorite = async (skinId: number) => {
try {
const response = await toggleFavorite(skinId);
if (response.code === 200) {
// 重新加载收藏数据
const favoritesResponse = await getFavoriteTextures({ page: 1, page_size: 50 });
if (favoritesResponse.code === 200) {
setFavoriteSkins(favoritesResponse.data.list || []);
}
}
} catch (error) {
console.error('切换收藏状态失败:', error);
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setSelectedFile(file);
}
};
const handleUploadSkin = async () => {
if (!selectedFile || !newSkinData.name.trim()) {
alert('请选择皮肤文件并输入皮肤名称');
return;
}
setIsUploading(true);
setUploadProgress(0);
try {
// 使用直接上传接口
const progressInterval = setInterval(() => {
setUploadProgress(prev => Math.min(prev + 10, 80));
}, 200);
const response = await uploadTexture(selectedFile, {
name: newSkinData.name.trim(),
description: newSkinData.description.trim(),
type: newSkinData.type,
is_public: newSkinData.is_public,
is_slim: newSkinData.is_slim
});
clearInterval(progressInterval);
setUploadProgress(100);
if (response.code === 200) {
// 重新加载皮肤数据
const mySkinsResponse = await getMyTextures({ page: 1, page_size: 50 });
if (mySkinsResponse.code === 200) {
setMySkins(mySkinsResponse.data.list || []);
}
// 重置表单
setSelectedFile(null);
setNewSkinData({
name: '',
description: '',
type: 'SKIN',
is_public: false,
is_slim: false
});
setShowUploadSkin(false);
alert('皮肤上传成功!');
} else {
throw new Error(response.message || '上传皮肤失败');
}
} catch (error) {
console.error('上传皮肤失败:', error);
alert(error instanceof Error ? error.message : '上传皮肤失败,请稍后重试');
} finally {
setIsUploading(false);
setUploadProgress(0);
}
};
const handleAvatarFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
// 验证文件类型和大小
if (!file.type.startsWith('image/')) {
alert('请选择图片文件');
return;
}
if (file.size > 2 * 1024 * 1024) {
alert('文件大小不能超过2MB');
return;
}
setAvatarFile(file);
}
};
const handleUploadAvatar = async () => {
if (!avatarFile) return;
setIsUploadingAvatar(true);
setAvatarUploadProgress(0);
try {
// 获取上传URL
const uploadUrlResponse = await generateAvatarUploadUrl(avatarFile.name);
if (uploadUrlResponse.code !== 200) {
throw new Error(uploadUrlResponse.message || '获取上传URL失败');
}
const { post_url, form_data, avatar_url } = uploadUrlResponse.data;
// 模拟上传进度
const progressInterval = setInterval(() => {
setAvatarUploadProgress(prev => Math.min(prev + 20, 80));
}, 200);
// 上传文件到预签名URL
const formData = new FormData();
Object.entries(form_data).forEach(([key, value]) => {
formData.append(key, value as string);
});
formData.append('file', avatarFile);
const uploadResponse = await fetch(post_url, {
method: 'POST',
body: formData,
});
if (!uploadResponse.ok) {
throw new Error('文件上传失败');
}
clearInterval(progressInterval);
setAvatarUploadProgress(100);
// 更新用户头像URL
const updateResponse = await updateAvatarUrl(avatar_url);
if (updateResponse.code === 200) {
setUserProfile(prev => prev ? { ...prev, avatar: avatar_url } : null);
alert('头像上传成功!');
} else {
throw new Error(updateResponse.message || '更新头像URL失败');
}
} catch (error) {
console.error('头像上传失败:', error);
alert(error instanceof Error ? error.message : '头像上传失败,请稍后重试');
} finally {
setIsUploadingAvatar(false);
setAvatarUploadProgress(0);
setAvatarFile(null);
}
};
const handleDeleteAvatar = async () => {
if (!confirm('确定要删除头像吗?')) return;
try {
const response = await updateAvatarUrl('');
if (response.code === 200) {
setUserProfile(prev => prev ? { ...prev, avatar: undefined } : null);
alert('头像删除成功!');
} else {
throw new Error(response.message || '删除头像失败');
}
} catch (error) {
console.error('删除头像失败:', error);
alert(error instanceof Error ? error.message : '删除头像失败,请稍后重试');
}
};
const sidebarVariants = {
hidden: { x: -100, opacity: 0 },
visible: { x: 0, opacity: 1, transition: { duration: 0.5, ease: "easeOut" as const } }
};
const contentVariants = {
hidden: { x: 100, opacity: 0 },
visible: { x: 0, opacity: 1, transition: { duration: 0.5, ease: "easeOut" as const, delay: 0.1 } }
};
if (!isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-orange-50 to-amber-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div className="text-center">
<motion.div
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ duration: 0.6, type: "spring" }}
className="mb-8"
>
<div className="w-24 h-24 bg-gradient-to-br from-orange-400 via-amber-500 to-orange-600 rounded-3xl flex items-center justify-center shadow-2xl mx-auto">
<UserCircleIcon className="w-12 h-12 text-white" />
</div>
</motion.div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<p className="text-gray-600 dark:text-gray-400 text-lg"></p>
</div>
</div>
);
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-orange-50 to-amber-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div className="text-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="mb-8"
>
<div className="w-16 h-16 bg-gradient-to-br from-orange-400 via-amber-500 to-orange-600 rounded-full flex items-center justify-center shadow-xl mx-auto">
<Cog6ToothIcon className="w-8 h-8 text-white" />
</div>
</motion.div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">...</h2>
<p className="text-gray-600 dark:text-gray-400"></p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-orange-50 to-amber-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div className="text-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.6, type: "spring" }}
className="mb-8"
>
<div className="w-24 h-24 bg-gradient-to-br from-red-400 via-red-500 to-red-600 rounded-full flex items-center justify-center shadow-2xl mx-auto">
<XCircleIcon className="w-12 h-12 text-white" />
</div>
</motion.div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<p className="text-gray-600 dark:text-gray-400 text-lg mb-6">{error}</p>
<motion.button
onClick={loadUserData}
className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-6 py-3 rounded-xl flex items-center space-x-2 shadow-lg hover:shadow-xl transition-all duration-200 mx-auto"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<ArrowPathIcon className="w-5 h-5" />
<span></span>
</motion.button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-orange-50 to-amber-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
{/* Animated Background */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-gradient-to-br from-orange-400/20 to-amber-400/20 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-gradient-to-tr from-pink-400/20 to-orange-400/20 rounded-full blur-3xl animate-pulse delay-1000"></div>
</div>
<div className="relative z-0">
<div className="flex min-h-screen">
{/* Left Sidebar - 完全固定的 */}
<motion.div
variants={sidebarVariants}
initial="hidden"
animate="visible"
className="w-80 bg-white/80 dark:bg-gray-800/80 backdrop-blur-lg border-r border-gray-200/50 dark:border-gray-700/50 p-6 fixed left-0 top-16 h-[calc(100vh-64px)] flex flex-col justify-between"
style={{ position: 'fixed', left: 0, top: '64px', height: 'calc(100vh - 64px)', width: '320px' }}
>
{/* User Profile Card */}
<div className="bg-gradient-to-br from-orange-400 via-orange-500 to-amber-500 rounded-2xl p-6 mb-6 text-white shadow-xl">
<div className="flex items-center space-x-4 mb-4">
{userProfile?.avatar ? (
<img
src={userProfile.avatar}
alt={userProfile.username}
className="w-16 h-16 rounded-full border-3 border-white/30 shadow-lg"
/>
) : (
<div className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center">
<UserCircleIcon className="w-8 h-8 text-white" />
</div>
)}
<div>
<h2 className="text-xl font-bold">{userProfile?.username}</h2>
<p className="text-white/80 text-sm">{userProfile?.email}</p>
</div>
</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-2xl font-bold">{mySkins.length}</div>
<div className="text-white/80 text-xs"></div>
</div>
<div>
<div className="text-2xl font-bold">{favoriteSkins.length}</div>
<div className="text-white/80 text-xs"></div>
</div>
<div>
<div className="text-2xl font-bold">{userProfile?.points || 0}</div>
<div className="text-white/80 text-xs"></div>
</div>
</div>
</div>
{/* Navigation Menu - 固定的 */}
<nav className="flex-1 overflow-y-auto">
<div className="space-y-2">
{[
{ id: 'characters', name: '角色管理', icon: UserIcon, count: profiles.length },
{ id: 'skins', name: '我的皮肤', icon: PhotoIcon, count: mySkins.length },
{ id: 'favorites', name: '收藏夹', icon: HeartIcon, count: favoriteSkins.length },
{ id: 'settings', name: '账户设置', icon: Cog6ToothIcon },
].map((item) => (
<motion.button
key={item.id}
onClick={() => setActiveTab(item.id as any)}
className={`w-full flex items-center justify-between p-3 rounded-xl transition-all duration-200 ${
activeTab === item.id
? 'bg-gradient-to-r from-orange-500 to-amber-500 text-white shadow-lg'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center space-x-3">
<item.icon className="w-5 h-5" />
<span className="font-medium">{item.name}</span>
</div>
{item.count !== undefined && (
<span className={`px-2 py-1 rounded-full text-xs ${
activeTab === item.id
? 'bg-white/20 text-white'
: 'bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-400'
}`}>
{item.count}
</span>
)}
</motion.button>
))}
</div>
</nav>
{/* Logout Button - 始终在底部可见 */}
<motion.button
onClick={logout}
className="w-full flex items-center justify-center space-x-2 p-3 border border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-xl transition-all duration-200 mt-6"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<ArrowLeftOnRectangleIcon className="w-5 h-5" />
<span>退</span>
</motion.button>
</motion.div>
{/* Right Content Area - 考虑左侧fixed侧栏的空间 */}
<motion.div
variants={contentVariants}
initial="hidden"
animate="visible"
className="flex-1 p-8 overflow-y-auto ml-80"
style={{ marginLeft: '320px', minHeight: 'calc(100vh - 64px)' }}
>
<AnimatePresence mode="wait">
{/* Characters Tab */}
{activeTab === 'characters' && (
<motion.div
key="characters"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="space-y-6"
>
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"></h1>
<motion.button
onClick={() => setShowCreateCharacter(true)}
className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-xl flex items-center space-x-2 shadow-lg hover:shadow-xl transition-all duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<PlusIcon className="w-5 h-5" />
<span></span>
</motion.button>
</div>
{/* Create Character Modal */}
<AnimatePresence>
{showCreateCharacter && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white dark:bg-gray-800 rounded-2xl p-6 w-full max-w-md mx-4"
>
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-gray-900 dark:text-white"></h3>
<button
onClick={() => setShowCreateCharacter(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XMarkIcon className="w-6 h-6" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="text"
value={newCharacterName}
onChange={(e) => setNewCharacterName(e.target.value)}
className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent"
placeholder="请输入角色名称"
/>
</div>
<div className="flex space-x-3">
<button
onClick={() => setShowCreateCharacter(false)}
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
</button>
<button
onClick={handleCreateCharacter}
disabled={!newCharacterName.trim()}
className="flex-1 px-4 py-2 bg-gradient-to-r from-orange-500 to-amber-500 text-white rounded-xl hover:from-orange-600 hover:to-amber-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
</button>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{profiles.length === 0 ? (
<div className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl p-12 text-center">
<UserIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2"></h3>
<p className="text-gray-600 dark:text-gray-400">Minecraft角色吧</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{profiles.map((profile) => (
<motion.div
key={profile.uuid}
className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl p-6 border border-white/20 dark:border-gray-700/50 shadow-lg"
whileHover={{ scale: 1.02, y: -5 }}
transition={{ duration: 0.2 }}
>
<div className="flex items-center justify-between mb-4">
{editingProfile === profile.uuid ? (
<input
type="text"
value={editProfileName}
onChange={(e) => setEditProfileName(e.target.value)}
className="text-lg font-semibold bg-transparent border-b border-orange-500 focus:outline-none text-gray-900 dark:text-white"
onBlur={() => handleEditCharacter(profile.uuid)}
onKeyPress={(e) => e.key === 'Enter' && handleEditCharacter(profile.uuid)}
autoFocus
/>
) : (
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{profile.name}</h3>
)}
{profile.is_active && (
<span className="px-2 py-1 bg-gradient-to-r from-green-500 to-emerald-500 text-white text-xs rounded-full">
使
</span>
)}
</div>
<div className="aspect-square bg-gradient-to-br from-orange-100 to-amber-100 dark:from-gray-700 dark:to-gray-600 rounded-xl mb-4 flex items-center justify-center">
<div className="w-20 h-20 bg-gradient-to-br from-orange-400 to-amber-500 rounded-lg shadow-lg"></div>
</div>
<div className="flex gap-2">
{!profile.is_active && (
<button
onClick={() => handleSetActiveCharacter(profile.uuid)}
className="flex-1 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white text-sm py-2 px-3 rounded-lg transition-all duration-200"
>
使
</button>
)}
<button
onClick={() => {
setEditingProfile(profile.uuid);
setEditProfileName(profile.name);
}}
className="flex-1 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 text-sm py-2 px-3 rounded-lg transition-all duration-200"
>
</button>
<button
onClick={() => handleDeleteCharacter(profile.uuid)}
className="px-3 py-2 border border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-lg transition-all duration-200"
>
<TrashIcon className="w-4 h-4" />
</button>
</div>
</motion.div>
))}
</div>
)}
</motion.div>
)}
{/* Skins Tab */}
{activeTab === 'skins' && (
<motion.div
key="skins"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="space-y-6"
>
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"></h1>
<motion.button
onClick={() => setShowUploadSkin(true)}
className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-xl flex items-center space-x-2 shadow-lg hover:shadow-xl transition-all duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<CloudArrowUpIcon className="w-5 h-5" />
<span></span>
</motion.button>
</div>
{/* Upload Skin Modal */}
<AnimatePresence>
{showUploadSkin && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white dark:bg-gray-800 rounded-2xl p-6 w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto"
>
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-gray-900 dark:text-white"></h3>
<button
onClick={() => setShowUploadSkin(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XMarkIcon className="w-6 h-6" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl p-6 text-center hover:border-orange-500 transition-colors">
<CloudArrowUpIcon className="w-12 h-12 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
</p>
<input
type="file"
accept=".png"
onChange={handleFileSelect}
className="hidden"
id="skin-upload"
/>
<label
htmlFor="skin-upload"
className="cursor-pointer bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-lg hover:from-orange-600 hover:to-amber-600 transition-all"
>
</label>
{selectedFile && (
<p className="text-sm text-green-600 dark:text-green-400 mt-2">
: {selectedFile.name}
</p>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="text"
value={newSkinData.name}
onChange={(e) => setNewSkinData(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent"
placeholder="请输入皮肤名称"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<textarea
value={newSkinData.description}
onChange={(e) => setNewSkinData(prev => ({ ...prev, description: e.target.value }))}
className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent"
placeholder="请输入皮肤描述(可选)"
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={newSkinData.is_public}
onChange={(e) => setNewSkinData(prev => ({ ...prev, is_public: e.target.checked }))}
className="w-4 h-4 text-orange-500 rounded focus:ring-orange-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300"></span>
</label>
</div>
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={newSkinData.is_slim}
onChange={(e) => setNewSkinData(prev => ({ ...prev, is_slim: e.target.checked }))}
className="w-4 h-4 text-orange-500 rounded focus:ring-orange-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300"></span>
</label>
</div>
</div>
{isUploading && (
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-gradient-to-r from-orange-500 to-amber-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
/>
</div>
)}
<div className="flex space-x-3">
<button
onClick={() => setShowUploadSkin(false)}
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
</button>
<button
onClick={handleUploadSkin}
disabled={!selectedFile || !newSkinData.name.trim() || isUploading}
className="flex-1 px-4 py-2 bg-gradient-to-r from-orange-500 to-amber-500 text-white rounded-xl hover:from-orange-600 hover:to-amber-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{isUploading ? `上传中... ${uploadProgress}%` : '上传'}
</button>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{mySkins.length === 0 ? (
<div className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl p-12 text-center">
<PhotoIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2"></h3>
<p className="text-gray-600 dark:text-gray-400">Minecraft皮肤吧</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{mySkins.map((skin) => (
<motion.div
key={skin.id}
className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl overflow-hidden border border-white/20 dark:border-gray-700/50 shadow-lg"
whileHover={{ scale: 1.02, y: -5 }}
transition={{ duration: 0.2 }}
>
<div className="aspect-square bg-gradient-to-br from-orange-100 to-amber-100 dark:from-gray-700 dark:to-gray-600 relative">
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-24 h-24 bg-gradient-to-br from-orange-400 to-amber-500 rounded-lg shadow-lg"></div>
</div>
{!skin.is_public && (
<div className="absolute top-3 right-3 px-2 py-1 bg-gray-800/80 text-white text-xs rounded-full backdrop-blur-sm">
</div>
)}
</div>
<div className="p-4">
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">{skin.name}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{new Date(skin.created_at).toLocaleDateString()}
</p>
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400 mb-4">
<span className="flex items-center space-x-1">
<ArrowDownTrayIcon className="w-4 h-4" />
<span>{skin.download_count}</span>
</span>
<span className="flex items-center space-x-1">
<EyeIcon className="w-4 h-4" />
<span>{skin.favorite_count}</span>
</span>
</div>
<div className="flex gap-2">
<button className="flex-1 bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 text-white text-sm py-2 px-3 rounded-lg transition-all duration-200">
</button>
<button
onClick={() => handleToggleSkinVisibility(skin.id)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-all duration-200"
>
{skin.is_public ? '隐藏' : '公开'}
</button>
<button
onClick={() => handleDeleteSkin(skin.id)}
className="px-3 py-2 border border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-lg transition-all duration-200"
>
<TrashIcon className="w-4 h-4" />
</button>
</div>
</div>
</motion.div>
))}
</div>
)}
</motion.div>
)}
{/* Favorites Tab */}
{activeTab === 'favorites' && (
<motion.div
key="favorites"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="space-y-6"
>
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"></h1>
</div>
{favoriteSkins.length === 0 ? (
<div className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl p-12 text-center">
<HeartIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2"></h3>
<p className="text-gray-600 dark:text-gray-400"></p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{favoriteSkins.map((skin) => (
<motion.div
key={skin.id}
className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl overflow-hidden border border-white/20 dark:border-gray-700/50 shadow-lg"
whileHover={{ scale: 1.02, y: -5 }}
transition={{ duration: 0.2 }}
>
<div className="aspect-square bg-gradient-to-br from-orange-100 to-amber-100 dark:from-gray-700 dark:to-gray-600 relative">
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-24 h-24 bg-gradient-to-br from-orange-400 to-amber-500 rounded-lg shadow-lg"></div>
</div>
<div className="absolute top-3 right-3 px-2 py-1 bg-red-500/80 text-white text-xs rounded-full backdrop-blur-sm flex items-center space-x-1">
<HeartIcon className="w-3 h-3" />
<span></span>
</div>
</div>
<div className="p-4">
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">{skin.name}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{skin.uploader_id}
</p>
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400 mb-4">
<span className="flex items-center space-x-1">
<ArrowDownTrayIcon className="w-4 h-4" />
<span>{skin.download_count}</span>
</span>
<span className="flex items-center space-x-1">
<EyeIcon className="w-4 h-4" />
<span>{skin.favorite_count}</span>
</span>
</div>
<div className="flex gap-2">
<button className="flex-1 bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 text-white text-sm py-2 px-3 rounded-lg transition-all duration-200">
</button>
<button
onClick={() => handleToggleFavorite(skin.id)}
className="px-3 py-2 border border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-lg transition-all duration-200"
>
</button>
</div>
</div>
</motion.div>
))}
</div>
)}
</motion.div>
)}
{/* Settings Tab */}
{activeTab === 'settings' && (
<motion.div
key="settings"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="space-y-6"
>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"></h1>
<div className="space-y-6">
{/* Avatar Settings */}
<motion.div
className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl p-6 border border-white/20 dark:border-gray-700/50 shadow-lg"
whileHover={{ y: -2 }}
transition={{ duration: 0.2 }}
>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center space-x-2">
<PhotoIcon className="w-5 h-5" />
<span></span>
</h3>
<div className="flex items-center space-x-6">
<div className="relative">
{userProfile?.avatar ? (
<img
src={userProfile.avatar}
alt={userProfile.username}
className="w-20 h-20 rounded-full border-4 border-orange-500 shadow-lg"
/>
) : (
<div className="w-20 h-20 bg-gradient-to-br from-orange-400 to-amber-500 rounded-full flex items-center justify-center shadow-lg">
<UserCircleIcon className="w-10 h-10 text-white" />
</div>
)}
<motion.button
className="absolute -bottom-2 -right-2 bg-gradient-to-r from-orange-500 to-amber-500 text-white p-2 rounded-full shadow-lg"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => document.getElementById('avatar-upload')?.click()}
>
<PencilIcon className="w-4 h-4" />
</motion.button>
<input
type="file"
id="avatar-upload"
accept="image/*"
onChange={handleAvatarFileSelect}
className="hidden"
/>
</div>
<div className="flex-1">
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
JPGPNG 2MB
</p>
{avatarFile && (
<div className="mb-3">
<p className="text-sm text-green-600 dark:text-green-400">
: {avatarFile.name}
</p>
</div>
)}
{isUploadingAvatar && (
<div className="mb-3">
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-gradient-to-r from-orange-500 to-amber-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${avatarUploadProgress}%` }}
/>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
... {avatarUploadProgress}%
</p>
</div>
)}
<div className="flex space-x-3">
<motion.button
onClick={handleUploadAvatar}
disabled={!avatarFile || isUploadingAvatar}
className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-xl flex items-center space-x-2 shadow-lg hover:shadow-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<CloudArrowUpIcon className="w-4 h-4" />
<span>{isUploadingAvatar ? '上传中...' : '上传头像'}</span>
</motion.button>
{userProfile?.avatar && (
<motion.button
onClick={handleDeleteAvatar}
disabled={isUploadingAvatar}
className="border border-red-500 text-red-500 hover:bg-red-500 hover:text-white px-4 py-2 rounded-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
</motion.button>
)}
</div>
</div>
</div>
</motion.div>
{/* Basic Info */}
<motion.div
className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl p-6 border border-white/20 dark:border-gray-700/50 shadow-lg"
whileHover={{ y: -2 }}
transition={{ duration: 0.2 }}
>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center space-x-2">
<UserIcon className="w-5 h-5" />
<span></span>
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="text"
value={userProfile?.username || ''}
readOnly
className="w-full px-4 py-3 bg-white/50 dark:bg-gray-700/50 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all duration-200"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="email"
value={userProfile?.email || ''}
readOnly
className="w-full px-4 py-3 bg-white/50 dark:bg-gray-700/50 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all duration-200"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="text"
value={userProfile ? new Date(userProfile.created_at).toLocaleDateString() : ''}
readOnly
className="w-full px-4 py-3 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-xl text-gray-500 dark:text-gray-400"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="text"
value={userProfile?.last_login_at ? new Date(userProfile.last_login_at).toLocaleString() : '从未登录'}
readOnly
className="w-full px-4 py-3 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-xl text-gray-500 dark:text-gray-400"
/>
</div>
</div>
<div className="mt-4">
<motion.button
className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-xl flex items-center space-x-2 shadow-lg hover:shadow-xl transition-all duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<PencilIcon className="w-4 h-4" />
<span></span>
</motion.button>
</div>
</motion.div>
{/* Password Settings */}
<motion.div
className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl p-6 border border-white/20 dark:border-gray-700/50 shadow-lg"
whileHover={{ y: -2 }}
transition={{ duration: 0.2 }}
>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center space-x-2">
<KeyIcon className="w-5 h-5" />
<span></span>
</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="password"
className="w-full px-4 py-3 bg-white/50 dark:bg-gray-700/50 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all duration-200"
placeholder="请输入当前密码"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="password"
className="w-full px-4 py-3 bg-white/50 dark:bg-gray-700/50 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all duration-200"
placeholder="请输入新密码"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="password"
className="w-full px-4 py-3 bg-white/50 dark:bg-gray-700/50 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all duration-200"
placeholder="请确认新密码"
/>
</div>
</div>
<motion.button
className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-xl flex items-center space-x-2 shadow-lg hover:shadow-xl transition-all duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<KeyIcon className="w-4 h-4" />
<span></span>
</motion.button>
</div>
</motion.div>
{/* API Settings */}
<motion.div
className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl p-6 border border-white/20 dark:border-gray-700/50 shadow-lg"
whileHover={{ y: -2 }}
transition={{ duration: 0.2 }}
>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center space-x-2">
<KeyIcon className="w-5 h-5" />
<span>API设置</span>
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Yggdrasil API密钥
</label>
<div className="flex gap-2">
<input
type="password"
value="your-api-key-here"
readOnly
className="flex-1 px-4 py-3 bg-white/50 dark:bg-gray-700/50 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all duration-200"
/>
<motion.button
className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-xl shadow-lg hover:shadow-xl transition-all duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
</motion.button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Minecraft客户端连接
</p>
</div>
</div>
</motion.div>
{/* Account Actions */}
<motion.div
className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl p-6 border border-white/20 dark:border-gray-700/50 shadow-lg"
whileHover={{ y: -2 }}
transition={{ duration: 0.2 }}
>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center space-x-2">
<Cog6ToothIcon className="w-5 h-5" />
<span></span>
</h3>
<div className="space-y-3">
<motion.button
className="w-full flex items-center justify-between p-3 border border-orange-500 text-orange-500 hover:bg-orange-500 hover:text-white rounded-xl transition-all duration-200"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span></span>
<EnvelopeIcon className="w-5 h-5" />
</motion.button>
<motion.button
onClick={logout}
className="w-full flex items-center justify-between p-3 border border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-xl transition-all duration-200"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span>退</span>
<ArrowLeftOnRectangleIcon className="w-5 h-5" />
</motion.button>
</div>
</motion.div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div>
</div>
</div>
);
}