'use client'; import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useAuth } from '@/contexts/AuthContext'; import { UserCircleIcon, Cog6ToothIcon, XCircleIcon, ArrowPathIcon, PlusIcon, XMarkIcon, UserIcon, PhotoIcon, HeartIcon, TrashIcon, PencilIcon, KeyIcon, EnvelopeIcon, CloudArrowUpIcon, EyeIcon, ArrowDownTrayIcon, ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline'; import { getMyTextures, getFavoriteTextures, toggleFavorite, getProfiles, createProfile, updateProfile, deleteProfile, setActiveProfile, getUserProfile, updateUserProfile, uploadTexture, getTexture, generateAvatarUploadUrl, updateAvatarUrl, resetYggdrasilPassword, type Texture, type Profile } from '@/lib/api'; import { messageManager } from '@/components/MessageNotification'; // 导入新的组件 import UserProfileCard from '@/components/profile/UserProfileCard'; import ProfileSidebar from '@/components/profile/ProfileSidebar'; import MySkinsTab from '@/components/profile/MySkinsTab'; import FavoritesTab from '@/components/profile/FavoritesTab'; import UploadSkinModal from '@/components/profile/UploadSkinModal'; import SkinViewer from '@/components/SkinViewer'; // Added SkinViewer import import CharacterCard from '@/components/profile/CharacterCard'; // Added CharacterCard import 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([]); const [mySkins, setMySkins] = useState([]); const [favoriteSkins, setFavoriteSkins] = useState([]); const [userProfile, setUserProfile] = useState(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(null); const [editingProfile, setEditingProfile] = useState(null); const [editProfileName, setEditProfileName] = useState(''); const [uploadProgress, setUploadProgress] = useState(0); const [avatarFile, setAvatarFile] = useState(null); const [isUploadingAvatar, setIsUploadingAvatar] = useState(false); const [avatarUploadProgress, setAvatarUploadProgress] = useState(0); const [error, setError] = useState(null); const [profileSkins, setProfileSkins] = useState>({}); const [showSkinSelector, setShowSkinSelector] = useState(null); const [yggdrasilPassword, setYggdrasilPassword] = useState(''); const [showYggdrasilPassword, setShowYggdrasilPassword] = useState(false); const [isResettingYggdrasilPassword, setIsResettingYggdrasilPassword] = useState(false); 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); // 加载每个角色的皮肤信息 const skinPromises = profilesResponse.data .filter(profile => profile.skin_id) .map(async (profile) => { try { const skinResponse = await getTexture(profile.skin_id!); if (skinResponse.code === 200 && skinResponse.data) { return { uuid: profile.uuid, url: skinResponse.data.url, isSlim: skinResponse.data.is_slim }; } } catch (error) { console.error(`加载角色 ${profile.uuid} 的皮肤失败:`, error); } return null; }); const skinResults = await Promise.all(skinPromises); const skinMap: Record = {}; skinResults.forEach((result) => { if (result) { skinMap[result.uuid] = { url: result.url, isSlim: result.isSlim }; } }); setProfileSkins(skinMap); } 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 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 handleDeleteSkin = async (skinId: number) => { if (!confirm('确定要删除这个皮肤吗?')) return; setMySkins(prev => prev.filter(skin => skin.id !== skinId)); }; 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) => { const file = e.target.files?.[0]; if (file) { setSelectedFile(file); } }; const handleUploadSkin = async (file: File, data: { name: string; description: string; type: 'SKIN' | 'CAPE'; is_public: boolean; is_slim: boolean }) => { if (!file || !data.name.trim()) { messageManager.warning('请选择皮肤文件并输入皮肤名称', { duration: 3000 }); return; } setIsUploading(true); setUploadProgress(0); try { // 使用直接上传接口 const progressInterval = setInterval(() => { setUploadProgress(prev => Math.min(prev + 10, 80)); }, 200); const response = await uploadTexture(file, { name: data.name.trim(), description: data.description.trim(), type: data.type, is_public: data.is_public, is_slim: data.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); messageManager.success('皮肤上传成功!', { duration: 3000 }); } else { throw new Error(response.message || '上传皮肤失败'); } } catch (error) { console.error('上传皮肤失败:', error); messageManager.error(error instanceof Error ? error.message : '上传皮肤失败,请稍后重试', { duration: 3000 }); } finally { setIsUploading(false); setUploadProgress(0); } }; const handleAvatarFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; // 检查文件类型 if (!file.type.startsWith('image/')) { messageManager.warning('请选择图片文件', { duration: 3000 }); return; } // 检查文件大小 (2MB) if (file.size > 2 * 1024 * 1024) { messageManager.warning('文件大小不能超过2MB', { duration: 3000 }); 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 response = await updateAvatarUrl(avatar_url); if (response.code === 200) { setUserProfile(prev => prev ? { ...prev, avatar: avatar_url } : null); messageManager.success('头像上传成功!', { duration: 3000 }); } else { throw new Error(response.message || '更新头像URL失败'); } } catch (error) { console.error('头像上传失败:', error); messageManager.error(error instanceof Error ? error.message : '头像上传失败,请稍后重试', { duration: 3000 }); } finally { setIsUploadingAvatar(false); setAvatarUploadProgress(0); setAvatarFile(null); } }; const handleDeleteAvatar = async () => { if (!confirm('确定要删除头像吗?')) return; try { const response = await updateUserProfile({ avatar: '' }); if (response.code === 200) { setUserProfile(response.data); messageManager.success('头像删除成功!', { duration: 3000 }); } else { throw new Error(response.message || '删除头像失败'); } } catch (error) { console.error('删除头像失败:', error); messageManager.error(error instanceof Error ? error.message : '删除头像失败,请稍后重试', { duration: 3000 }); } finally { setAvatarFile(null); } }; const handleResetYggdrasilPassword = async () => { if (!confirm('确定要重置Yggdrasil密码吗?这将生成一个新的密码。')) return; setIsResettingYggdrasilPassword(true); try { const response = await resetYggdrasilPassword(); if (response.code === 200) { messageManager.success('Yggdrasil密码重置成功!请妥善保管新密码。', { duration: 5000 }); } else { throw new Error(response.message || '重置Yggdrasil密码失败'); } } catch (error) { console.error('重置Yggdrasil密码失败:', error); messageManager.error(error instanceof Error ? error.message : '重置Yggdrasil密码失败,请稍后重试', { duration: 3000 }); } finally { setIsResettingYggdrasilPassword(false); } }; const handleCreateCharacter = async () => { if (!newCharacterName.trim()) { messageManager.warning('请输入角色名称', { duration: 3000 }); return; } try { const response = await createProfile(newCharacterName.trim()); if (response.code === 200) { setProfiles(prev => [...prev, response.data]); setNewCharacterName(''); messageManager.success('角色创建成功!', { duration: 3000 }); } else { throw new Error(response.message || '创建角色失败'); } } catch (error) { console.error('创建角色失败:', error); messageManager.error(error instanceof Error ? error.message : '创建角色失败,请稍后重试', { duration: 3000 }); } finally { } }; 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)); messageManager.success('角色删除成功!', { duration: 3000 }); } else { throw new Error(response.message || '删除角色失败'); } } catch (error) { console.error('删除角色失败:', error); messageManager.error(error instanceof Error ? error.message : '删除角色失败,请稍后重试', { duration: 3000 }); } finally { } }; 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 }))); messageManager.success('角色切换成功!', { duration: 3000 }); } else { throw new Error(response.message || '设置活跃角色失败'); } } catch (error) { console.error('设置活跃角色失败:', error); messageManager.error(error instanceof Error ? error.message : '设置活跃角色失败,请稍后重试', { duration: 3000 }); } finally { } }; const handleEditCharacter = async () => { if (!editProfileName.trim()) { messageManager.warning('请输入角色名称', { duration: 3000 }); return; } try { const response = await updateProfile(editingProfile!, { name: editProfileName.trim() }); if (response.code === 200) { setProfiles(prev => prev.map(profile => profile.uuid === editingProfile ? response.data : profile )); setEditingProfile(null); setEditProfileName(''); messageManager.success('角色编辑成功!', { duration: 3000 }); } else { throw new Error(response.message || '编辑角色失败'); } } catch (error) { console.error('编辑角色失败:', error); messageManager.error(error instanceof Error ? error.message : '编辑角色失败,请稍后重试', { duration: 3000 }); } }; const onEdit = (uuid: string, currentName: string) => { setEditingProfile(uuid); setEditProfileName(currentName); }; const onCancelEdit = () => { setEditingProfile(null); setEditProfileName(''); }; // 渲染内容区域 const renderContent = () => { switch (activeTab) { case 'characters': return (

角色管理

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 }} > 创建角色
{/* Create Character Modal */} {showCreateCharacter && (

创建新角色

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="请输入角色名称" />
)}
{profiles.length === 0 ? (

暂无角色

创建你的第一个Minecraft角色吧!

) : (
{profiles.map((profile) => ( ))}
)}
); case 'skins': return ( setShowUploadSkin(true)} onToggleVisibility={handleToggleSkinVisibility} onDelete={handleDeleteSkin} /> ); case 'favorites': return ( ); case 'settings': return (

账户设置

{/* Avatar Settings */}

头像设置

{userProfile?.avatar ? ( {userProfile.username} ) : (
)} document.getElementById('avatar-upload')?.click()} >

支持 JPG、PNG 格式,最大 2MB

{avatarFile && (

已选择: {avatarFile.name}

)} {isUploadingAvatar && (

上传中... {avatarUploadProgress}%

)}
{isUploadingAvatar ? '上传中...' : '上传头像'} {userProfile?.avatar && ( 删除头像 )}
{/* Yggdrasil Settings */}

Yggdrasil 设置

setShowYggdrasilPassword(!showYggdrasilPassword)} className="bg-gradient-to-r from-gray-500 to-gray-600 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 }} > {showYggdrasilPassword ? '隐藏' : '显示'} {isResettingYggdrasilPassword ? '重置中...' : '重置密码'}

此密码用于Minecraft客户端连接,请妥善保管

{/* Basic Info */}

基本信息

{/* Account Actions */}

账户操作

更换邮箱地址 退出登录
); default: return (

账户设置

功能开发中

该功能正在开发中,敬请期待!

); } }; // 渲染皮肤选择器模态框 const renderSkinSelector = () => { if (!showSkinSelector) return null; const currentProfile = profiles.find(p => p.uuid === showSkinSelector); const availableSkins = mySkins.filter(skin => skin.type === 'SKIN'); return (

为角色 "{currentProfile?.name}" 选择皮肤

{ // 移除皮肤 if (currentProfile) { updateProfile(currentProfile.uuid, { skin_id: undefined }); setProfiles(prev => prev.map(p => p.uuid === currentProfile.uuid ? { ...p, skin_id: undefined } : p )); setProfileSkins(prev => { const newSkins = { ...prev }; delete newSkins[currentProfile.uuid]; return newSkins; }); setShowSkinSelector(null); } }} >

移除皮肤

{availableSkins.map((skin) => ( { // 分配皮肤给角色 if (currentProfile) { updateProfile(currentProfile.uuid, { skin_id: skin.id }); setProfiles(prev => prev.map(p => p.uuid === currentProfile.uuid ? { ...p, skin_id: skin.id } : p )); setProfileSkins(prev => ({ ...prev, [currentProfile.uuid]: { url: skin.url, isSlim: skin.is_slim } })); setShowSkinSelector(null); } }} >
{skin.name}
))}
{availableSkins.length === 0 && (

暂无可用皮肤,请先上传皮肤

)}
); }; if (!isAuthenticated) { return (

请先登录

登录后即可查看个人资料

); } if (isLoading) { return (

加载中...

正在加载您的个人资料

); } if (error) { return (

加载失败

{error}

重新加载
); } return (
{/* Animated Background */}
{/* Left Sidebar - 使用CSS自定义属性考虑navbar高度 */} {/* User Profile Card */} {/* Profile Sidebar Component */} setActiveTab(tab as 'characters' | 'skins' | 'favorites' | 'settings')} skinCount={mySkins.length} favoriteCount={favoriteSkins.length} profilesCount={profiles.length} onLogout={logout} /> {/* Right Content Area - 考虑左侧fixed侧栏的空间 */} {renderContent()}
{/* Upload Skin Modal */} setShowUploadSkin(false)} onUpload={async (file, data) => { setSelectedFile(file); setNewSkinData(data); await handleUploadSkin(file, data); }} isUploading={isUploading} uploadProgress={uploadProgress} /> {/* Skin Selector Modal */} {renderSkinSelector()}
); }