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>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|