- 优化navbar滚动隐藏逻辑,更敏感响应 - 添加返回顶部按钮,固定在右下角 - 实现profile页面侧边栏真正冻结效果 - 修复首页滑动指示器位置 - 优化整体布局确保首屏内容完整显示
1360 lines
66 KiB
TypeScript
1360 lines
66 KiB
TypeScript
'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">
|
||
支持 JPG、PNG 格式,最大 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>
|
||
);
|
||
}
|
||
|