diff --git a/src/app/globals.css b/src/app/globals.css
index 12290ab..580ead7 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -8,6 +8,11 @@
--background: #ffffff;
--foreground: #171717;
--navbar-height: 64px; /* 与pt-16对应 */
+ --primary-orange: #f97316;
+ --primary-orange-dark: #ea580c;
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-normal: 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
}
@media (prefers-color-scheme: dark) {
@@ -21,6 +26,7 @@ body {
color: var(--foreground);
background: var(--background);
font-family: 'Inter', Arial, Helvetica, sans-serif;
+ scroll-behavior: smooth;
}
/* Custom utility classes */
@@ -28,34 +34,65 @@ body {
text-wrap: balance;
}
-/* Custom component classes */
+/* Enhanced Custom component classes with micro-interactions */
.btn-carrot {
- background-color: #f97316;
+ background-color: var(--primary-orange);
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
- transition: background-color 0.2s;
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+ transition: all var(--transition-normal);
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ position: relative;
+ overflow: hidden;
}
.btn-carrot:hover {
- background-color: #ea580c;
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+ background-color: var(--primary-orange-dark);
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ transform: translateY(-2px);
+}
+
+.btn-carrot:active {
+ transform: translateY(0);
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+}
+
+.btn-carrot::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+ transition: left var(--transition-slow);
+}
+
+.btn-carrot:hover::before {
+ left: 100%;
}
.btn-carrot-outline {
- border: 2px solid #f97316;
- color: #f97316;
+ border: 2px solid var(--primary-orange);
+ color: var(--primary-orange);
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
- transition: all 0.2s;
+ transition: all var(--transition-normal);
+ position: relative;
+ overflow: hidden;
}
.btn-carrot-outline:hover {
- background-color: #f97316;
+ background-color: var(--primary-orange);
color: white;
+ transform: translateY(-2px);
+ box-shadow: 0 10px 15px -3px rgba(249, 115, 22, 0.3);
+}
+
+.btn-carrot-outline:active {
+ transform: translateY(0);
}
.card-minecraft {
@@ -63,11 +100,31 @@ body {
border: 2px solid #fed7aa;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
- transition: all 0.2s;
+ transition: all var(--transition-normal);
+ position: relative;
+ overflow: hidden;
}
.card-minecraft:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+ transform: translateY(-4px);
+ border-color: var(--primary-orange);
+}
+
+.card-minecraft::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(135deg, rgba(249, 115, 22, 0.1) 0%, transparent 50%);
+ opacity: 0;
+ transition: opacity var(--transition-normal);
+}
+
+.card-minecraft:hover::after {
+ opacity: 1;
}
@media (prefers-color-scheme: dark) {
@@ -75,6 +132,10 @@ body {
background-color: #1f2937;
border-color: #c2410c;
}
+
+ .card-minecraft:hover {
+ border-color: var(--primary-orange);
+ }
}
.text-gradient {
@@ -82,10 +143,18 @@ body {
background-clip: text;
-webkit-background-clip: text;
color: transparent;
+ transition: all var(--transition-normal);
+}
+
+.text-gradient:hover {
+ background: linear-gradient(to right, #f97316, #ea580c);
+ background-clip: text;
+ -webkit-background-clip: text;
}
.bg-gradient-carrot {
background: linear-gradient(to bottom right, #fb923c, #f97316, #ea580c);
+ transition: all var(--transition-normal);
}
/* 现代布局解决方案 */
@@ -104,4 +173,277 @@ body {
.min-h-screen-nav {
min-height: calc(100vh - var(--navbar-height));
}
+
+ /* 增强的过渡效果 */
+ .transition-all-enhanced {
+ transition: all var(--transition-normal);
+ }
+
+ .transition-colors-enhanced {
+ transition: color var(--transition-normal), background-color var(--transition-normal), border-color var(--transition-normal);
+ }
+
+ .transition-transform-enhanced {
+ transition: transform var(--transition-normal);
+ }
+
+ /* 微交互效果 */
+ .micro-interaction {
+ transition: all var(--transition-fast);
+ }
+
+ .micro-interaction:hover {
+ transform: scale(1.02);
+ }
+
+ .micro-interaction:active {
+ transform: scale(0.98);
+ }
+
+ /* 加载动画 */
+ .animate-pulse-slow {
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+ }
+
+ .animate-pulse-fast {
+ animation: pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+ }
+
+ /* 弹跳动画 */
+ .animate-bounce-slow {
+ animation: bounce 2s infinite;
+ }
+
+ .animate-bounce-fast {
+ animation: bounce 1s infinite;
+ }
+
+ /* 旋转动画 */
+ .animate-spin-slow {
+ animation: spin 3s linear infinite;
+ }
+
+ .animate-spin-fast {
+ animation: spin 1s linear infinite;
+ }
+
+ /* 渐变动画 */
+ .animate-gradient {
+ background-size: 200% 200%;
+ animation: gradient 3s ease infinite;
+ }
+
+ /* 阴影动画 */
+ .shadow-animated {
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ transition: box-shadow var(--transition-normal);
+ }
+
+ .shadow-animated:hover {
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ }
+
+ /* 模糊动画 */
+ .backdrop-blur-animated {
+ backdrop-filter: blur(8px);
+ transition: backdrop-filter var(--transition-normal);
+ }
+
+ .backdrop-blur-animated:hover {
+ backdrop-filter: blur(16px);
+ }
+}
+
+/* 自定义关键帧动画 */
+@keyframes gradient {
+ 0% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+ 100% {
+ background-position: 0% 50%;
+ }
+}
+
+@keyframes float {
+ 0%, 100% {
+ transform: translateY(0px);
+ }
+ 50% {
+ transform: translateY(-10px);
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
+
+@keyframes slideInUp {
+ from {
+ transform: translateY(30px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slideInDown {
+ from {
+ transform: translateY(-30px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slideInLeft {
+ from {
+ transform: translateX(-30px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slideInRight {
+ from {
+ transform: translateX(30px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes scaleIn {
+ from {
+ transform: scale(0.9);
+ opacity: 0;
+ }
+ to {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+@keyframes scaleOut {
+ from {
+ transform: scale(1);
+ opacity: 1;
+ }
+ to {
+ transform: scale(0.9);
+ opacity: 0;
+ }
+}
+
+/* 动画工具类 */
+.animate-slide-in-up {
+ animation: slideInUp 0.3s ease-out;
+}
+
+.animate-slide-in-down {
+ animation: slideInDown 0.3s ease-out;
+}
+
+.animate-slide-in-left {
+ animation: slideInLeft 0.3s ease-out;
+}
+
+.animate-slide-in-right {
+ animation: slideInRight 0.3s ease-out;
+}
+
+.animate-scale-in {
+ animation: scaleIn 0.2s ease-out;
+}
+
+.animate-scale-out {
+ animation: scaleOut 0.2s ease-out;
+}
+
+/* 加载状态样式 */
+.loading-shimmer {
+ background: linear-gradient(
+ 90deg,
+ #f0f0f0 0%,
+ #e0e0e0 50%,
+ #f0f0f0 100%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+}
+
+.dark .loading-shimmer {
+ background: linear-gradient(
+ 90deg,
+ #374151 0%,
+ #4b5563 50%,
+ #374151 100%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+}
+
+/* 焦点样式 */
+.focus-visible-enhanced {
+ outline: 2px solid var(--primary-orange);
+ outline-offset: 2px;
+}
+
+/* 滚动条样式 */
+.custom-scrollbar::-webkit-scrollbar {
+ width: 6px;
+}
+
+.custom-scrollbar::-webkit-scrollbar-track {
+ background: rgba(0, 0, 0, 0.1);
+ border-radius: 3px;
+}
+
+.custom-scrollbar::-webkit-scrollbar-thumb {
+ background: var(--primary-orange);
+ border-radius: 3px;
+}
+
+.custom-scrollbar::-webkit-scrollbar-thumb:hover {
+ background: var(--primary-orange-dark);
+}
+
+/* 响应式动效 */
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* 触摸设备优化 */
+@media (hover: none) and (pointer: coarse) {
+ .btn-carrot:hover,
+ .btn-carrot-outline:hover,
+ .card-minecraft:hover {
+ transform: none;
+ }
+
+ .btn-carrot:active,
+ .btn-carrot-outline:active,
+ .card-minecraft:active {
+ transform: scale(0.98);
+ }
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 41af848..8096f60 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -4,8 +4,10 @@ import "./globals.css";
import Navbar from "@/components/Navbar";
import { AuthProvider } from "@/contexts/AuthContext";
import { MainContent } from "@/components/MainContent";
+import { MessageNotificationContainer } from "@/components/MessageNotification";
import { ErrorNotificationContainer } from "@/components/ErrorNotification";
import ScrollToTop from "@/components/ScrollToTop";
+import PageTransition from "@/components/PageTransition";
const inter = Inter({
subsets: ["latin"],
@@ -35,8 +37,11 @@ export default function RootLayout({
- {children}
+
+ {children}
+
+
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
index fff1627..eb8a890 100644
--- a/src/app/not-found.tsx
+++ b/src/app/not-found.tsx
@@ -1,108 +1,7 @@
'use client';
-import Link from 'next/link';
-import { motion } from 'framer-motion';
-import { HomeIcon, ArrowLeftIcon } from '@heroicons/react/24/outline';
+import { NotFoundPage } from '@/components/ErrorPage';
export default function NotFound() {
- return (
-
-
- {/* 404 数字 */}
-
-
- 404
-
-
-
-
- {/* 错误信息 */}
-
-
- 页面不见了
-
-
- 抱歉,我们找不到您要访问的页面。它可能已被移动、删除,或者您输入的链接不正确。
-
-
-
- {/* Minecraft 风格的装饰 */}
-
-
-
-
- {/* 操作按钮 */}
-
-
-
- 返回主页
-
-
-
-
-
- {/* 额外的帮助信息 */}
-
- 如果问题持续存在,请
-
- 联系我们
-
- 的支持团队
-
-
-
-
- {/* 背景装饰 */}
-
-
- );
+ return ;
}
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx
index 5db2902..a03b9fb 100644
--- a/src/app/profile/page.tsx
+++ b/src/app/profile/page.tsx
@@ -2,27 +2,26 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
-import {
- UserCircleIcon,
- PencilIcon,
- TrashIcon,
- PlusIcon,
- EyeIcon,
- ArrowDownTrayIcon,
+import { useAuth } from '@/contexts/AuthContext';
+import {
+ UserCircleIcon,
Cog6ToothIcon,
+ XCircleIcon,
+ ArrowPathIcon,
+ PlusIcon,
+ XMarkIcon,
UserIcon,
PhotoIcon,
+ HeartIcon,
+ TrashIcon,
+ PencilIcon,
KeyIcon,
EnvelopeIcon,
-
- HeartIcon,
- ArrowLeftOnRectangleIcon,
CloudArrowUpIcon,
- XMarkIcon,
- ArrowPathIcon,
- XCircleIcon
+ EyeIcon,
+ ArrowDownTrayIcon,
+ ArrowLeftOnRectangleIcon
} from '@heroicons/react/24/outline';
-import { useAuth } from '@/contexts/AuthContext';
import {
getMyTextures,
getFavoriteTextures,
@@ -42,7 +41,16 @@ import {
type Texture,
type Profile
} from '@/lib/api';
-import SkinViewer from '@/components/SkinViewer';
+import { messageManager } from '@/components/MessageNotification';
+
+// 导入新的组件
+import UserProfileCard from '@/components/profile/UserProfileCard';
+import ProfileSidebar from '@/components/profile/ProfileSidebar';
+import MySkinsTab from '@/components/profile/MySkinsTab';
+import FavoritesTab from '@/components/profile/FavoritesTab';
+import UploadSkinModal from '@/components/profile/UploadSkinModal';
+import SkinViewer from '@/components/SkinViewer'; // Added SkinViewer import
+import CharacterCard from '@/components/profile/CharacterCard'; // Added CharacterCard import
interface UserProfile {
id: number;
@@ -169,101 +177,6 @@ export default function ProfilePage() {
}
};
- 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));
- setProfileSkins(prev => {
- const newSkins = { ...prev };
- delete newSkins[uuid];
- return newSkins;
- });
- 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);
@@ -278,6 +191,12 @@ export default function ProfilePage() {
}
};
+ const handleDeleteSkin = async (skinId: number) => {
+ if (!confirm('确定要删除这个皮肤吗?')) return;
+
+ setMySkins(prev => prev.filter(skin => skin.id !== skinId));
+ };
+
const handleToggleFavorite = async (skinId: number) => {
try {
const response = await toggleFavorite(skinId);
@@ -300,9 +219,9 @@ export default function ProfilePage() {
}
};
- const handleUploadSkin = async () => {
- if (!selectedFile || !newSkinData.name.trim()) {
- alert('请选择皮肤文件并输入皮肤名称');
+ const handleUploadSkin = async (file: File, data: { name: string; description: string; type: 'SKIN' | 'CAPE'; is_public: boolean; is_slim: boolean }) => {
+ if (!file || !data.name.trim()) {
+ messageManager.warning('请选择皮肤文件并输入皮肤名称', { duration: 3000 });
return;
}
@@ -315,12 +234,12 @@ export default function ProfilePage() {
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
+ const response = await uploadTexture(file, {
+ name: data.name.trim(),
+ description: data.description.trim(),
+ type: data.type,
+ is_public: data.is_public,
+ is_slim: data.is_slim
});
clearInterval(progressInterval);
@@ -343,14 +262,14 @@ export default function ProfilePage() {
is_slim: false
});
setShowUploadSkin(false);
- alert('皮肤上传成功!');
+ messageManager.success('皮肤上传成功!', { duration: 3000 });
} else {
throw new Error(response.message || '上传皮肤失败');
}
} catch (error) {
console.error('上传皮肤失败:', error);
- alert(error instanceof Error ? error.message : '上传皮肤失败,请稍后重试');
+ messageManager.error(error instanceof Error ? error.message : '上传皮肤失败,请稍后重试', { duration: 3000 });
} finally {
setIsUploading(false);
setUploadProgress(0);
@@ -359,18 +278,20 @@ export default function ProfilePage() {
const handleAvatarFileSelect = (e: React.ChangeEvent) => {
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);
+ if (!file) return;
+
+ // 检查文件类型
+ if (!file.type.startsWith('image/')) {
+ messageManager.warning('请选择图片文件', { duration: 3000 });
+ return;
}
+
+ // 检查文件大小 (2MB)
+ if (file.size > 2 * 1024 * 1024) {
+ messageManager.warning('文件大小不能超过2MB', { duration: 3000 });
+ return;
+ }
+ setAvatarFile(file);
};
const handleUploadAvatar = async () => {
@@ -413,17 +334,17 @@ export default function ProfilePage() {
setAvatarUploadProgress(100);
// 更新用户头像URL
- const updateResponse = await updateAvatarUrl(avatar_url);
- if (updateResponse.code === 200) {
+ const response = await updateAvatarUrl(avatar_url);
+ if (response.code === 200) {
setUserProfile(prev => prev ? { ...prev, avatar: avatar_url } : null);
- alert('头像上传成功!');
+ messageManager.success('头像上传成功!', { duration: 3000 });
} else {
- throw new Error(updateResponse.message || '更新头像URL失败');
+ throw new Error(response.message || '更新头像URL失败');
}
} catch (error) {
console.error('头像上传失败:', error);
- alert(error instanceof Error ? error.message : '头像上传失败,请稍后重试');
+ messageManager.error(error instanceof Error ? error.message : '头像上传失败,请稍后重试', { duration: 3000 });
} finally {
setIsUploadingAvatar(false);
setAvatarUploadProgress(0);
@@ -438,13 +359,15 @@ export default function ProfilePage() {
const response = await updateUserProfile({ avatar: '' });
if (response.code === 200) {
setUserProfile(response.data);
- alert('头像删除成功!');
+ messageManager.success('头像删除成功!', { duration: 3000 });
} else {
throw new Error(response.message || '删除头像失败');
}
} catch (error) {
console.error('删除头像失败:', error);
- alert(error instanceof Error ? error.message : '删除头像失败,请稍后重试');
+ messageManager.error(error instanceof Error ? error.message : '删除头像失败,请稍后重试', { duration: 3000 });
+ } finally {
+ setAvatarFile(null);
}
};
@@ -456,85 +379,607 @@ export default function ProfilePage() {
try {
const response = await resetYggdrasilPassword();
if (response.code === 200) {
- setYggdrasilPassword(response.data.password);
- setShowYggdrasilPassword(true);
- alert('Yggdrasil密码重置成功!请妥善保管新密码。');
+ messageManager.success('Yggdrasil密码重置成功!请妥善保管新密码。', { duration: 5000 });
} else {
throw new Error(response.message || '重置Yggdrasil密码失败');
}
} catch (error) {
console.error('重置Yggdrasil密码失败:', error);
- alert(error instanceof Error ? error.message : '重置Yggdrasil密码失败,请稍后重试');
+ messageManager.error(error instanceof Error ? error.message : '重置Yggdrasil密码失败,请稍后重试', { duration: 3000 });
} finally {
setIsResettingYggdrasilPassword(false);
}
};
- const handleAssignSkin = async (profileUuid: string, skinId: number) => {
+ const handleCreateCharacter = async () => {
+ if (!newCharacterName.trim()) {
+ messageManager.warning('请输入角色名称', { duration: 3000 });
+ return;
+ }
+
try {
- const response = await updateProfile(profileUuid, { skin_id: skinId });
+ const response = await createProfile(newCharacterName.trim());
if (response.code === 200) {
- // 更新角色数据
- setProfiles(prev => prev.map(profile =>
- profile.uuid === profileUuid ? response.data : profile
- ));
-
- // 更新皮肤显示
- const skinResponse = await getTexture(skinId);
- if (skinResponse.code === 200 && skinResponse.data) {
- setProfileSkins(prev => ({
- ...prev,
- [profileUuid]: {
- url: skinResponse.data.url,
- isSlim: skinResponse.data.is_slim
- }
- }));
- }
-
- setShowSkinSelector(null);
- alert('皮肤配置成功!');
+ setProfiles(prev => [...prev, response.data]);
+ setNewCharacterName('');
+ messageManager.success('角色创建成功!', { duration: 3000 });
} else {
- throw new Error(response.message || '配置皮肤失败');
+ throw new Error(response.message || '创建角色失败');
}
} catch (error) {
- console.error('配置皮肤失败:', error);
- alert(error instanceof Error ? error.message : '配置皮肤失败,请稍后重试');
+ console.error('创建角色失败:', error);
+ messageManager.error(error instanceof Error ? error.message : '创建角色失败,请稍后重试', { duration: 3000 });
+ } finally {
}
};
- const handleRemoveSkin = async (profileUuid: string) => {
+ 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 updateProfile(profileUuid, { skin_id: undefined });
+ const response = await deleteProfile(uuid);
if (response.code === 200) {
- setProfiles(prev => prev.map(profile =>
- profile.uuid === profileUuid ? response.data : profile
- ));
-
- // 移除皮肤显示
- setProfileSkins(prev => {
- const newSkins = { ...prev };
- delete newSkins[profileUuid];
- return newSkins;
- });
-
- alert('皮肤移除成功!');
+ setProfiles(prev => prev.filter(profile => profile.uuid !== uuid));
+ messageManager.success('角色删除成功!', { duration: 3000 });
} else {
- throw new Error(response.message || '移除皮肤失败');
+ throw new Error(response.message || '删除角色失败');
}
} catch (error) {
- console.error('移除皮肤失败:', error);
- alert(error instanceof Error ? error.message : '移除皮肤失败,请稍后重试');
+ console.error('删除角色失败:', error);
+ messageManager.error(error instanceof Error ? error.message : '删除角色失败,请稍后重试', { duration: 3000 });
+ } finally {
}
};
- const sidebarVariants = {
- hidden: { x: -100, opacity: 0 },
- visible: { x: 0, opacity: 1, transition: { duration: 0.5, ease: "easeOut" as const } }
+ const handleSetActiveCharacter = async (uuid: string) => {
+ try {
+ const response = await setActiveProfile(uuid);
+ if (response.code === 200) {
+ setProfiles(prev => prev.map(profile => ({
+ ...profile,
+ is_active: profile.uuid === uuid
+ })));
+ messageManager.success('角色切换成功!', { duration: 3000 });
+ } else {
+ throw new Error(response.message || '设置活跃角色失败');
+ }
+ } catch (error) {
+ console.error('设置活跃角色失败:', error);
+ messageManager.error(error instanceof Error ? error.message : '设置活跃角色失败,请稍后重试', { duration: 3000 });
+ } finally {
+ }
};
- const contentVariants = {
- hidden: { x: 100, opacity: 0 },
- visible: { x: 0, opacity: 1, transition: { duration: 0.5, ease: "easeOut" as const, delay: 0.1 } }
+ const handleEditCharacter = async () => {
+ if (!editProfileName.trim()) {
+ messageManager.warning('请输入角色名称', { duration: 3000 });
+ return;
+ }
+
+ try {
+ const response = await updateProfile(editingProfile!, { name: editProfileName.trim() });
+ if (response.code === 200) {
+ setProfiles(prev => prev.map(profile =>
+ profile.uuid === editingProfile ? response.data : profile
+ ));
+ setEditingProfile(null);
+ setEditProfileName('');
+ messageManager.success('角色编辑成功!', { duration: 3000 });
+ } else {
+ throw new Error(response.message || '编辑角色失败');
+ }
+ } catch (error) {
+ console.error('编辑角色失败:', error);
+ messageManager.error(error instanceof Error ? error.message : '编辑角色失败,请稍后重试', { duration: 3000 });
+ }
+ };
+
+ const onEdit = (uuid: string, currentName: string) => {
+ setEditingProfile(uuid);
+ setEditProfileName(currentName);
+ };
+
+ const onCancelEdit = () => {
+ setEditingProfile(null);
+ setEditProfileName('');
+ };
+
+ // 渲染内容区域
+ const renderContent = () => {
+ switch (activeTab) {
+ case 'characters':
+ return (
+
+
+
角色管理
+
setShowCreateCharacter(true)}
+ className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-xl flex items-center space-x-2 shadow-lg hover:shadow-xl transition-all duration-200"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+ 创建角色
+
+
+
+ {/* Create Character Modal */}
+
+ {showCreateCharacter && (
+
+
+
+
创建新角色
+
+
+
+
+
+ setNewCharacterName(e.target.value)}
+ className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent"
+ placeholder="请输入角色名称"
+ />
+
+
+
+
+
+
+
+
+ )}
+
+
+ {profiles.length === 0 ? (
+
+
+
暂无角色
+
创建你的第一个Minecraft角色吧!
+
+ ) : (
+
+ {profiles.map((profile) => (
+
+ ))}
+
+ )}
+
+ );
+ case 'skins':
+ return (
+ setShowUploadSkin(true)}
+ onToggleVisibility={handleToggleSkinVisibility}
+ onDelete={handleDeleteSkin}
+ />
+ );
+ case 'favorites':
+ return (
+
+ );
+ case 'settings':
+ return (
+
+ 账户设置
+
+
+ {/* Avatar Settings */}
+
+
+
+ 头像设置
+
+
+
+ {userProfile?.avatar ? (
+

+ ) : (
+
+
+
+ )}
+
document.getElementById('avatar-upload')?.click()}
+ >
+
+
+
+
+
+
+ 支持 JPG、PNG 格式,最大 2MB
+
+ {avatarFile && (
+
+
+ 已选择: {avatarFile.name}
+
+
+ )}
+ {isUploadingAvatar && (
+
+
+
+ 上传中... {avatarUploadProgress}%
+
+
+ )}
+
+
+
+ {isUploadingAvatar ? '上传中...' : '上传头像'}
+
+ {userProfile?.avatar && (
+
+ 删除头像
+
+ )}
+
+
+
+
+
+ {/* Yggdrasil Settings */}
+
+
+
+ Yggdrasil 设置
+
+
+
+
+
+
+ setShowYggdrasilPassword(!showYggdrasilPassword)}
+ className="bg-gradient-to-r from-gray-500 to-gray-600 text-white px-4 py-2 rounded-xl shadow-lg hover:shadow-xl transition-all duration-200"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ {showYggdrasilPassword ? '隐藏' : '显示'}
+
+
+ {isResettingYggdrasilPassword ? '重置中...' : '重置密码'}
+
+
+
+ 此密码用于Minecraft客户端连接,请妥善保管
+
+
+
+
+
+ {/* Basic Info */}
+
+
+
+ 基本信息
+
+
+
+
+ {/* Account Actions */}
+
+
+
+ 账户操作
+
+
+
+ 更换邮箱地址
+
+
+
+ 退出登录
+
+
+
+
+
+
+ );
+ default:
+ return (
+
+
+ 账户设置
+
+
+
功能开发中
+
该功能正在开发中,敬请期待!
+
+
+ );
+ }
+ };
+
+ // 渲染皮肤选择器模态框
+ const renderSkinSelector = () => {
+ if (!showSkinSelector) return null;
+
+ const currentProfile = profiles.find(p => p.uuid === showSkinSelector);
+ const availableSkins = mySkins.filter(skin => skin.type === 'SKIN');
+
+ return (
+
+
+
+
+
+ 为角色 "{currentProfile?.name}" 选择皮肤
+
+
+
+
+
+
{
+ // 移除皮肤
+ if (currentProfile) {
+ updateProfile(currentProfile.uuid, { skin_id: undefined });
+ setProfiles(prev => prev.map(p =>
+ p.uuid === currentProfile.uuid ? { ...p, skin_id: undefined } : p
+ ));
+ setProfileSkins(prev => {
+ const newSkins = { ...prev };
+ delete newSkins[currentProfile.uuid];
+ return newSkins;
+ });
+ setShowSkinSelector(null);
+ }
+ }}
+ >
+
+
+
+ {availableSkins.map((skin) => (
+
{
+ // 分配皮肤给角色
+ if (currentProfile) {
+ updateProfile(currentProfile.uuid, { skin_id: skin.id });
+ setProfiles(prev => prev.map(p =>
+ p.uuid === currentProfile.uuid ? { ...p, skin_id: skin.id } : p
+ ));
+ setProfileSkins(prev => ({
+ ...prev,
+ [currentProfile.uuid]: { url: skin.url, isSlim: skin.is_slim }
+ }));
+ setShowSkinSelector(null);
+ }
+ }}
+ >
+
+
+ {skin.name}
+
+
+ ))}
+
+
+ {availableSkins.length === 0 && (
+
+ )}
+
+
+
+ );
};
if (!isAuthenticated) {
@@ -611,1059 +1056,78 @@ export default function ProfilePage() {
return (
{/* Animated Background */}
-
+
-
+
- {/* Left Sidebar - 完全固定的 */}
+ {/* Left Sidebar - 使用CSS自定义属性考虑navbar高度 */}
{/* User Profile Card */}
-
-
- {userProfile?.avatar ? (
-

- ) : (
-
-
-
- )}
-
-
{userProfile?.username}
-
{userProfile?.email}
-
-
-
-
-
-
{mySkins.length}
-
皮肤
-
-
-
{favoriteSkins.length}
-
收藏
-
-
-
{userProfile?.points || 0}
-
积分
-
-
-
+
- {/* Navigation Menu - 固定的 */}
-
-
- {/* Logout Button - 始终在底部可见 */}
-
-
- 退出登录
-
+ {/* Profile Sidebar Component */}
+ setActiveTab(tab as 'characters' | 'skins' | 'favorites' | 'settings')}
+ skinCount={mySkins.length}
+ favoriteCount={favoriteSkins.length}
+ profilesCount={profiles.length}
+ onLogout={logout}
+ />
{/* Right Content Area - 考虑左侧fixed侧栏的空间 */}
- {/* Characters Tab */}
- {activeTab === 'characters' && (
-
-
-
角色管理
-
setShowCreateCharacter(true)}
- className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-xl flex items-center space-x-2 shadow-lg hover:shadow-xl transition-all duration-200"
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- >
-
- 创建角色
-
-
-
- {/* Create Character Modal */}
-
- {showCreateCharacter && (
-
-
-
-
创建新角色
-
-
-
-
-
- setNewCharacterName(e.target.value)}
- className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent"
- placeholder="请输入角色名称"
- />
-
-
-
-
-
-
-
-
- )}
-
-
- {/* Skin Selector Modal */}
-
- {showSkinSelector && (
-
-
-
-
选择皮肤
-
-
-
- {mySkins.length === 0 ? (
-
-
-
暂无皮肤
-
您还没有上传任何皮肤
-
-
- ) : (
- <>
-
-
-
-
-
- {mySkins.map((skin) => (
-
handleAssignSkin(showSkinSelector, skin.id)}
- >
-
- {skin.type === 'SKIN' ? (
-
- ) : (
-
- )}
- {!skin.is_public && (
-
- 私密
-
- )}
-
-
-
-
{skin.name}
-
- {skin.type === 'SKIN' ? (skin.is_slim ? '细臂模型' : '经典模型') : '披风'}
-
-
-
-
- {skin.download_count}
-
-
-
- {skin.favorite_count}
-
-
-
-
- ))}
-
- >
- )}
-
-
- )}
-
-
- {profiles.length === 0 ? (
-
-
-
暂无角色
-
创建你的第一个Minecraft角色吧!
-
- ) : (
-
- {profiles.map((profile) => (
-
-
- {editingProfile === profile.uuid ? (
- 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
- />
- ) : (
-
{profile.name}
- )}
- {profile.is_active && (
-
- 当前使用
-
- )}
-
-
-
- {profileSkins[profile.uuid] ? (
-
- ) : (
-
- )}
-
-
-
- {!profile.is_active && (
-
- )}
-
-
-
-
-
- ))}
-
- )}
-
- )}
-
- {/* Skins Tab */}
- {activeTab === 'skins' && (
-
-
-
我的皮肤
- 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 }}
- >
-
- 上传皮肤
-
-
-
- {/* Upload Skin Modal */}
-
- {showUploadSkin && (
-
-
-
-
上传皮肤
-
-
-
-
-
-
-
-
- 点击选择文件或拖拽到此处
-
-
-
- {selectedFile && (
-
- 已选择: {selectedFile.name}
-
- )}
-
-
-
-
-
- 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="请输入皮肤名称"
- />
-
-
-
-
-
-
-
-
- {isUploading && (
-
- )}
-
-
-
-
-
-
-
-
- )}
-
-
- {mySkins.length === 0 ? (
-
-
-
暂无皮肤
-
上传你的第一个Minecraft皮肤吧!
-
- ) : (
-
- {mySkins.map((skin) => (
-
-
- {skin.type === 'SKIN' ? (
-
- ) : (
-
- )}
- {!skin.is_public && (
-
- 私密
-
- )}
-
-
-
-
{skin.name}
-
- 上传于 {new Date(skin.created_at).toLocaleDateString()}
-
-
-
-
-
- {skin.download_count}
-
-
-
- {skin.favorite_count}
-
-
-
-
-
-
-
-
-
-
- ))}
-
- )}
-
- )}
-
- {/* Favorites Tab */}
- {activeTab === 'favorites' && (
-
-
-
收藏夹
-
-
- {favoriteSkins.length === 0 ? (
-
-
-
暂无收藏
-
去发现一些喜欢的皮肤吧!
-
- ) : (
-
- {favoriteSkins.map((skin) => (
-
-
- {skin.type === 'SKIN' ? (
-
- ) : (
-
- )}
-
-
- 已收藏
-
-
-
-
-
{skin.name}
-
- 由 {skin.uploader_id} 上传
-
-
-
-
-
- {skin.download_count}
-
-
-
- {skin.favorite_count}
-
-
-
-
-
-
-
-
-
- ))}
-
- )}
-
- )}
-
- {/* Settings Tab */}
- {activeTab === 'settings' && (
-
- 账户设置
-
-
- {/* Avatar Settings */}
-
-
-
- 头像设置
-
-
-
- {userProfile?.avatar ? (
-

- ) : (
-
-
-
- )}
-
document.getElementById('avatar-upload')?.click()}
- >
-
-
-
-
-
-
- 支持 JPG、PNG 格式,最大 2MB
-
- {avatarFile && (
-
-
- 已选择: {avatarFile.name}
-
-
- )}
- {isUploadingAvatar && (
-
-
-
- 上传中... {avatarUploadProgress}%
-
-
- )}
-
-
-
- {isUploadingAvatar ? '上传中...' : '上传头像'}
-
- {userProfile?.avatar && (
-
- 删除头像
-
- )}
-
-
-
-
-
- {/* Basic Info */}
-
-
-
- 基本信息
-
-
-
-
-
- {/* Password Settings */}
-
-
-
- 密码设置
-
-
-
-
- {/* Yggdrasil Settings */}
-
-
-
- Yggdrasil设置
-
-
-
-
-
-
- setShowYggdrasilPassword(!showYggdrasilPassword)}
- className="bg-gradient-to-r from-gray-500 to-gray-600 text-white px-4 py-2 rounded-xl shadow-lg hover:shadow-xl transition-all duration-200"
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- disabled={!yggdrasilPassword}
- >
- {showYggdrasilPassword ? "隐藏" : "显示"}
-
-
- {isResettingYggdrasilPassword ? "重置中..." : "重置密码"}
-
-
-
- 此密码用于Minecraft客户端连接Yggdrasil认证系统,请妥善保管
-
- {yggdrasilPassword && (
-
-
- ⚠️ 重要提醒: 请立即复制并保存您的新密码,出于安全考虑,关闭此页面后将无法再次查看。
-
-
- )}
-
-
-
-
- {/* Account Actions */}
-
-
-
- 账户操作
-
-
-
- 更换邮箱地址
-
-
-
- 退出登录
-
-
-
-
-
-
- )}
+ {renderContent()}
+
+ {/* Upload Skin Modal */}
+
setShowUploadSkin(false)}
+ onUpload={async (file, data) => {
+ setSelectedFile(file);
+ setNewSkinData(data);
+ await handleUploadSkin(file, data);
+ }}
+ isUploading={isUploading}
+ uploadProgress={uploadProgress}
+ />
+
+ {/* Skin Selector Modal */}
+ {renderSkinSelector()}
);
}
diff --git a/src/app/skins/page.tsx b/src/app/skins/page.tsx
index 2c66dac..e6e1ae3 100644
--- a/src/app/skins/page.tsx
+++ b/src/app/skins/page.tsx
@@ -6,8 +6,10 @@ import { MagnifyingGlassIcon, EyeIcon, HeartIcon, ArrowDownTrayIcon, SparklesIco
import { HeartIcon as HeartIconSolid } from '@heroicons/react/24/solid';
import SkinViewer from '@/components/SkinViewer';
import SkinDetailModal from '@/components/SkinDetailModal';
+import SkinCard from '@/components/SkinCard';
import { searchTextures, toggleFavorite, type Texture } from '@/lib/api';
import { useAuth } from '@/contexts/AuthContext';
+import { messageManager } from '@/components/MessageNotification';
export default function SkinsPage() {
const [textures, setTextures] = useState
([]);
@@ -106,7 +108,7 @@ export default function SkinsPage() {
// 处理收藏
const handleFavorite = async (textureId: number) => {
if (!isAuthenticated) {
- alert('请先登录');
+ messageManager.warning('请先登录', { duration: 3000 });
return;
}
@@ -309,130 +311,15 @@ export default function SkinsPage() {
const isFavorited = favoritedIds.has(texture.id);
return (
-
- {/* 3D预览区域 - 更紧凑 */}
-
-
-
- {/* 悬停操作按钮 */}
-
-
-
handleDetailView(texture)}
- className="bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white p-3 rounded-full shadow-lg transition-all duration-200"
- whileHover={{ scale: 1.1 }}
- whileTap={{ scale: 0.9 }}
- title="详细预览"
- >
-
-
-
-
window.open(texture.url, '_blank')}
- className="bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white p-3 rounded-full shadow-lg transition-all duration-200"
- whileHover={{ scale: 1.1 }}
- whileTap={{ scale: 0.9 }}
- title="查看原图"
- >
-
-
-
-
-
- {/* 标签 */}
-
-
- {texture.type === 'SKIN' ? '皮肤' : '披风'}
-
- {texture.is_slim && (
-
- 细臂
-
- )}
-
-
-
- {/* Texture Info */}
-
-
{texture.name}
- {texture.description && (
-
- {texture.description}
-
- )}
-
- {/* Stats */}
-
-
-
-
- {texture.favorite_count}
-
-
-
- {texture.download_count}
-
-
-
-
- {/* Action Buttons */}
-
- handleDetailView(texture)}
- className="flex-1 bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white text-sm py-2 px-3 rounded-lg transition-all duration-200 font-medium shadow-md hover:shadow-lg flex items-center justify-center"
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
-
- 详细预览
-
- handleFavorite(texture.id)}
- className={`px-3 py-2 border rounded-lg transition-all duration-200 font-medium ${
- isFavorited
- ? 'bg-gradient-to-r from-red-500 to-pink-500 border-transparent text-white shadow-md'
- : 'border-orange-500 text-orange-500 hover:bg-gradient-to-r hover:from-orange-500 hover:to-orange-600 hover:text-white hover:border-transparent hover:shadow-md'
- }`}
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- >
- {isFavorited ? (
-
- ) : (
-
- )}
-
-
-
-
+ texture={texture}
+ isFavorited={isFavorited}
+ onViewDetails={handleDetailView}
+ onToggleFavorite={isAuthenticated ? handleFavorite : undefined}
+ onDownload={(texture) => window.open(texture.url, '_blank')}
+ showVisibilityBadge={false}
+ />
);
})}
diff --git a/src/components/EnhancedButton.tsx b/src/components/EnhancedButton.tsx
new file mode 100644
index 0000000..d7626b2
--- /dev/null
+++ b/src/components/EnhancedButton.tsx
@@ -0,0 +1,246 @@
+'use client';
+
+import { motion, MotionProps } from 'framer-motion';
+import { ReactNode, useState, useEffect } from 'react';
+import { AnimatePresence } from 'framer-motion';
+
+interface EnhancedButtonProps extends Omit {
+ children: ReactNode;
+ onClick?: (e: React.MouseEvent) => void | Promise;
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
+ size?: 'sm' | 'md' | 'lg';
+ disabled?: boolean;
+ loading?: boolean;
+ fullWidth?: boolean;
+ icon?: ReactNode;
+ iconPosition?: 'left' | 'right';
+ ripple?: boolean;
+ sound?: boolean;
+ haptic?: boolean;
+ className?: string;
+ type?: 'button' | 'submit' | 'reset';
+}
+
+export default function EnhancedButton({
+ children,
+ onClick,
+ variant = 'primary',
+ size = 'md',
+ disabled = false,
+ loading = false,
+ fullWidth = false,
+ icon,
+ iconPosition = 'left',
+ ripple = true,
+ sound = true,
+ haptic = true,
+ className = '',
+ type = 'button',
+ ...motionProps
+}: EnhancedButtonProps) {
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [ripples, setRipples] = useState>([]);
+
+ // 播放音效
+ const playSound = (type: 'click' | 'success' | 'error' = 'click') => {
+ if (!sound) return;
+
+ try {
+ const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ const frequencies = {
+ click: 800,
+ success: 1000,
+ error: 400
+ };
+
+ oscillator.frequency.setValueAtTime(frequencies[type], audioContext.currentTime);
+ oscillator.type = type === 'error' ? 'sawtooth' : 'sine';
+
+ gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
+
+ oscillator.start(audioContext.currentTime);
+ oscillator.stop(audioContext.currentTime + 0.2);
+ } catch (error) {
+ // 忽略音频API错误
+ }
+ };
+
+ // 触觉反馈
+ const triggerHaptic = () => {
+ if (haptic && navigator.vibrate) {
+ navigator.vibrate(50);
+ }
+ };
+
+ // 创建涟漪效果
+ const createRipple = (e: React.MouseEvent) => {
+ if (!ripple) return;
+
+ const button = e.currentTarget;
+ const rect = button.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+ const newRipple = { id: Date.now(), x, y };
+
+ setRipples(prev => [...prev, newRipple]);
+
+ // 移除涟漪
+ setTimeout(() => {
+ setRipples(prev => prev.filter(r => r.id !== newRipple.id));
+ }, 600);
+ };
+
+ // 处理点击事件
+ const handleClick = async (e: React.MouseEvent) => {
+ if (disabled || loading || isProcessing) return;
+
+ createRipple(e);
+ playSound('click');
+ triggerHaptic();
+
+ if (onClick) {
+ try {
+ setIsProcessing(true);
+ const result = onClick(e);
+
+ // 如果返回的是 Promise,等待它完成
+ if (result instanceof Promise) {
+ await result;
+ playSound('success');
+ }
+ } catch (error) {
+ playSound('error');
+ console.error('Button click error:', error);
+ } finally {
+ setIsProcessing(false);
+ }
+ }
+ };
+
+ const getVariantStyles = () => {
+ const baseStyles = 'relative overflow-hidden transition-all duration-200 font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
+
+ const variants = {
+ primary: 'bg-gradient-to-r from-orange-500 to-amber-500 text-white hover:from-orange-600 hover:to-amber-600 focus:ring-orange-500 shadow-lg hover:shadow-xl',
+ secondary: 'bg-gradient-to-r from-gray-500 to-gray-600 text-white hover:from-gray-600 hover:to-gray-700 focus:ring-gray-500 shadow-md hover:shadow-lg',
+ outline: 'border-2 border-orange-500 text-orange-500 hover:bg-orange-500 hover:text-white focus:ring-orange-500',
+ ghost: 'text-orange-500 hover:bg-orange-100 hover:text-orange-600 focus:ring-orange-500 dark:hover:bg-orange-900/20',
+ danger: 'bg-gradient-to-r from-red-500 to-pink-500 text-white hover:from-red-600 hover:to-pink-600 focus:ring-red-500 shadow-lg hover:shadow-xl'
+ };
+
+ const sizes = {
+ sm: 'px-3 py-1.5 text-sm rounded-md',
+ md: 'px-4 py-2 text-base rounded-lg',
+ lg: 'px-6 py-3 text-lg rounded-xl'
+ };
+
+ return `${baseStyles} ${variants[variant]} ${sizes[size]} ${fullWidth ? 'w-full' : ''} ${className}`;
+ };
+
+ const buttonVariants = {
+ initial: { scale: 1 },
+ hover: {
+ scale: 1.02,
+ transition: { duration: 0.2, ease: "easeOut" }
+ },
+ tap: {
+ scale: 0.98,
+ transition: { duration: 0.1 }
+ },
+ loading: {
+ scale: 0.95,
+ transition: { duration: 0.2 }
+ }
+ };
+
+ const isDisabled = disabled || loading || isProcessing;
+
+ return (
+
+ {/* 涟漪效果 */}
+ {ripple && (
+
+ {ripples.map(ripple => (
+
+ ))}
+
+ )}
+
+ {/* 加载状态 */}
+
+ {(loading || isProcessing) && (
+
+
+
+ )}
+
+
+ {/* 按钮内容 */}
+
+ {icon && iconPosition === 'left' && (
+
+ {icon}
+
+ )}
+ {children}
+ {icon && iconPosition === 'right' && (
+
+ {icon}
+
+ )}
+
+
+ );
+}
+
diff --git a/src/components/EnhancedInput.tsx b/src/components/EnhancedInput.tsx
new file mode 100644
index 0000000..06eb4ac
--- /dev/null
+++ b/src/components/EnhancedInput.tsx
@@ -0,0 +1,432 @@
+'use client';
+
+import { motion, AnimatePresence } from 'framer-motion';
+import { ReactNode, useState, useRef, useEffect, forwardRef } from 'react';
+import { EyeIcon, EyeSlashIcon, ExclamationCircleIcon, CheckCircleIcon } from '@heroicons/react/24/outline';
+
+interface EnhancedInputProps {
+ type?: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search';
+ placeholder?: string;
+ value?: string;
+ onChange?: (e: React.ChangeEvent) => void;
+ onFocus?: (e: React.FocusEvent) => void;
+ onBlur?: (e: React.FocusEvent) => void;
+ label?: string;
+ error?: string;
+ success?: string;
+ hint?: string;
+ disabled?: boolean;
+ required?: boolean;
+ leftIcon?: ReactNode;
+ rightIcon?: ReactNode;
+ className?: string;
+ containerClassName?: string;
+ autoFocus?: boolean;
+ autoComplete?: string;
+ name?: string;
+ id?: string;
+ showPasswordToggle?: boolean;
+ showStrengthIndicator?: boolean;
+ showCharCount?: boolean;
+ maxLength?: number;
+ minLength?: number;
+ pattern?: string;
+ validate?: (value: string) => string | null;
+ onValidationChange?: (isValid: boolean) => void;
+}
+
+const EnhancedInput = forwardRef(({
+ type = 'text',
+ placeholder,
+ value = '',
+ onChange,
+ onFocus,
+ onBlur,
+ label,
+ error,
+ success,
+ hint,
+ disabled = false,
+ required = false,
+ leftIcon,
+ rightIcon,
+ className = '',
+ containerClassName = '',
+ autoFocus = false,
+ autoComplete,
+ name,
+ id,
+ showPasswordToggle = false,
+ showStrengthIndicator = false,
+ showCharCount = false,
+ maxLength,
+ minLength,
+ pattern,
+ validate,
+ onValidationChange,
+ ...props
+}, ref) => {
+ const [isFocused, setIsFocused] = useState(false);
+ const [isPasswordVisible, setIsPasswordVisible] = useState(false);
+ const [internalValue, setInternalValue] = useState(value);
+ const [validationMessage, setValidationMessage] = useState(null);
+ const [isValidating, setIsValidating] = useState(false);
+ const inputRef = useRef(null);
+ const containerRef = useRef(null);
+
+ // 同步外部值
+ useEffect(() => {
+ setInternalValue(value);
+ }, [value]);
+
+ // 验证逻辑
+ useEffect(() => {
+ if (validate && internalValue) {
+ setIsValidating(true);
+ const message = validate(internalValue);
+ setValidationMessage(message);
+ setIsValidating(false);
+
+ if (onValidationChange) {
+ onValidationChange(!message);
+ }
+ }
+ }, [internalValue, validate, onValidationChange]);
+
+ // 密码强度计算
+ const getPasswordStrength = (password: string) => {
+ let strength = 0;
+ if (password.length >= 8) strength++;
+ if (/[a-z]/.test(password)) strength++;
+ if (/[A-Z]/.test(password)) strength++;
+ if (/[0-9]/.test(password)) strength++;
+ if (/[^A-Za-z0-9]/.test(password)) strength++;
+ return strength;
+ };
+
+ const passwordStrength = type === 'password' && showStrengthIndicator ? getPasswordStrength(internalValue) : 0;
+
+ const handleFocus = (e: React.FocusEvent) => {
+ setIsFocused(true);
+ onFocus?.(e);
+ };
+
+ const handleBlur = (e: React.FocusEvent) => {
+ setIsFocused(false);
+ onBlur?.(e);
+ };
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const newValue = e.target.value;
+ setInternalValue(newValue);
+ onChange?.(e);
+ };
+
+ const togglePasswordVisibility = () => {
+ setIsPasswordVisible(!isPasswordVisible);
+ };
+
+ const getInputType = () => {
+ if (type === 'password' && showPasswordToggle) {
+ return isPasswordVisible ? 'text' : 'password';
+ }
+ return type;
+ };
+
+ const getBorderColor = () => {
+ if (error || validationMessage) return 'border-red-500 focus:border-red-500 focus:ring-red-500';
+ if (success || (validationMessage === null && internalValue && validate)) return 'border-green-500 focus:border-green-500 focus:ring-green-500';
+ if (isFocused) return 'border-orange-500 focus:border-orange-500 focus:ring-orange-500';
+ return 'border-gray-300 dark:border-gray-600 focus:border-orange-500 focus:ring-orange-500';
+ };
+
+ const getLabelColor = () => {
+ if (error || validationMessage) return 'text-red-600 dark:text-red-400';
+ if (success || (validationMessage === null && internalValue && validate)) return 'text-green-600 dark:text-green-400';
+ if (isFocused) return 'text-orange-600 dark:text-orange-400';
+ return 'text-gray-700 dark:text-gray-300';
+ };
+
+ const inputVariants = {
+ initial: { scale: 1 },
+ focus: {
+ scale: 1.02,
+ transition: { duration: 0.2, ease: "easeOut" }
+ },
+ blur: {
+ scale: 1,
+ transition: { duration: 0.2, ease: "easeOut" }
+ }
+ };
+
+ const labelVariants = {
+ initial: { y: 0, scale: 1 },
+ focus: {
+ y: -20,
+ scale: 0.85,
+ transition: { duration: 0.2, ease: "easeOut" }
+ },
+ blur: {
+ y: internalValue ? -20 : 0,
+ scale: internalValue ? 0.85 : 1,
+ transition: { duration: 0.2, ease: "easeOut" }
+ }
+ };
+
+ return (
+
+ {/* 标签 */}
+ {label && (
+
+ {label}
+ {required && *}
+
+ )}
+
+ {/* 输入框容器 */}
+
+ {/* 左侧图标 */}
+ {leftIcon && (
+
+ {leftIcon}
+
+ )}
+
+ {/* 输入框 */}
+
+
+ {/* 验证状态图标 */}
+
+ {isValidating && (
+
+
+
+ )}
+
+ {!isValidating && validationMessage === null && internalValue && validate && (
+
+
+
+ )}
+
+ {(error || validationMessage) && (
+
+
+
+ )}
+
+
+ {/* 右侧图标和密码切换 */}
+
+ {type === 'password' && showPasswordToggle && (
+
+ {isPasswordVisible ? : }
+
+ )}
+
+ {rightIcon && (
+
+ {rightIcon}
+
+ )}
+
+
+
+ {/* 密码强度指示器 */}
+ {type === 'password' && showStrengthIndicator && internalValue && (
+
+
+ {[1, 2, 3, 4, 5].map((level) => (
+ = level
+ ? level <= 2
+ ? 'bg-red-500'
+ : level <= 3
+ ? 'bg-yellow-500'
+ : 'bg-green-500'
+ : 'bg-gray-200 dark:bg-gray-700'
+ }`}
+ initial={{ scaleX: 0 }}
+ animate={{ scaleX: passwordStrength >= level ? 1 : 0.3 }}
+ transition={{ delay: level * 0.1 }}
+ />
+ ))}
+
+
+ {passwordStrength <= 2 && '密码强度:弱'}
+ {passwordStrength === 3 && '密码强度:中等'}
+ {passwordStrength >= 4 && '密码强度:强'}
+
+
+ )}
+
+ {/* 字符计数 */}
+ {showCharCount && maxLength && (
+
+
+ maxLength
+ ? 'text-red-500'
+ : internalValue.length > maxLength * 0.8
+ ? 'text-yellow-500'
+ : 'text-gray-500'
+ }`}
+ animate={{
+ scale: internalValue.length > maxLength ? 1.1 : 1,
+ color: internalValue.length > maxLength ? '#ef4444' : internalValue.length > maxLength * 0.8 ? '#f59e0b' : '#6b7280'
+ }}
+ >
+ {internalValue.length}/{maxLength}
+
+
+ )}
+
+ {/* 错误信息 */}
+
+ {(error || validationMessage) && (
+
+
+ {error || validationMessage}
+
+ )}
+
+
+ {/* 成功信息 */}
+
+ {success && (
+
+
+ {success}
+
+ )}
+
+
+ {/* 提示信息 */}
+
+ {hint && !error && !validationMessage && !success && (
+
+ {hint}
+
+ )}
+
+
+ );
+});
+
+EnhancedInput.displayName = 'EnhancedInput';
+
+export default EnhancedInput;
diff --git a/src/components/ErrorNotification.tsx b/src/components/ErrorNotification.tsx
index 221a626..40ecd8f 100644
--- a/src/components/ErrorNotification.tsx
+++ b/src/components/ErrorNotification.tsx
@@ -15,16 +15,26 @@ interface ErrorNotificationProps {
export function ErrorNotification({ message, type = 'error', duration = 5000, onClose }: ErrorNotificationProps) {
const [isVisible, setIsVisible] = useState(true);
+ const [isHovered, setIsHovered] = useState(false);
+ const [progress, setProgress] = useState(100);
useEffect(() => {
- if (duration > 0) {
- const timer = setTimeout(() => {
- setIsVisible(false);
- onClose?.();
- }, duration);
- return () => clearTimeout(timer);
+ if (duration > 0 && !isHovered) {
+ const startTime = Date.now();
+ const timer = setInterval(() => {
+ const elapsed = Date.now() - startTime;
+ const remaining = Math.max(0, duration - elapsed);
+ setProgress((remaining / duration) * 100);
+
+ if (remaining === 0) {
+ setIsVisible(false);
+ onClose?.();
+ }
+ }, 50);
+
+ return () => clearInterval(timer);
}
- }, [duration, onClose]);
+ }, [duration, onClose, isHovered]);
const handleClose = () => {
setIsVisible(false);
@@ -52,7 +62,8 @@ export function ErrorNotification({ message, type = 'error', duration = 5000, on
border: 'border-red-200 dark:border-red-800',
text: 'text-red-800 dark:text-red-200',
icon: 'text-red-500',
- close: 'text-red-400 hover:text-red-600 dark:text-red-300 dark:hover:text-red-100'
+ close: 'text-red-400 hover:text-red-600 dark:text-red-300 dark:hover:text-red-100',
+ progress: 'bg-red-500'
};
case 'warning':
return {
@@ -60,7 +71,8 @@ export function ErrorNotification({ message, type = 'error', duration = 5000, on
border: 'border-yellow-200 dark:border-yellow-800',
text: 'text-yellow-800 dark:text-yellow-200',
icon: 'text-yellow-500',
- close: 'text-yellow-400 hover:text-yellow-600 dark:text-yellow-300 dark:hover:text-yellow-100'
+ close: 'text-yellow-400 hover:text-yellow-600 dark:text-yellow-300 dark:hover:text-yellow-100',
+ progress: 'bg-yellow-500'
};
case 'success':
return {
@@ -68,7 +80,8 @@ export function ErrorNotification({ message, type = 'error', duration = 5000, on
border: 'border-green-200 dark:border-green-800',
text: 'text-green-800 dark:text-green-200',
icon: 'text-green-500',
- close: 'text-green-400 hover:text-green-600 dark:text-green-300 dark:hover:text-green-100'
+ close: 'text-green-400 hover:text-green-600 dark:text-green-300 dark:hover:text-green-100',
+ progress: 'bg-green-500'
};
case 'info':
return {
@@ -76,38 +89,98 @@ export function ErrorNotification({ message, type = 'error', duration = 5000, on
border: 'border-blue-200 dark:border-blue-800',
text: 'text-blue-800 dark:text-blue-200',
icon: 'text-blue-500',
- close: 'text-blue-400 hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-100'
+ close: 'text-blue-400 hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-100',
+ progress: 'bg-blue-500'
};
}
};
const styles = getStyles();
+ const getAnimationVariants = () => {
+ switch (type) {
+ case 'error':
+ return {
+ initial: { opacity: 0, x: 100, scale: 0.8, rotate: -5 },
+ animate: { opacity: 1, x: 0, scale: 1, rotate: 0 },
+ exit: { opacity: 0, x: 100, scale: 0.8, rotate: 5 },
+ };
+ case 'warning':
+ return {
+ initial: { opacity: 0, y: -20, scale: 0.9 },
+ animate: { opacity: 1, y: 0, scale: 1 },
+ exit: { opacity: 0, y: -20, scale: 0.9 },
+ };
+ case 'success':
+ return {
+ initial: { opacity: 0, scale: 0.5 },
+ animate: { opacity: 1, scale: 1 },
+ exit: { opacity: 0, scale: 0.5 },
+ };
+ case 'info':
+ return {
+ initial: { opacity: 0, x: -20, scale: 0.9 },
+ animate: { opacity: 1, x: 0, scale: 1 },
+ exit: { opacity: 0, x: -20, scale: 0.9 },
+ };
+ }
+ };
+
return (
{isVisible && (
setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ whileHover={{ scale: 1.02, y: -2 }}
>
+ {/* Progress bar */}
+
+
+
+
-
+
{getIcon()}
-
+
-
+
)}
@@ -115,10 +188,11 @@ export function ErrorNotification({ message, type = 'error', duration = 5000, on
);
}
-// 全局错误管理器
+// 增强的全局错误管理器
class ErrorManager {
private static instance: ErrorManager;
private listeners: Array<(notification: ErrorNotificationProps & { id: string }) => void> = [];
+ private soundEnabled: boolean = true;
static getInstance(): ErrorManager {
if (!ErrorManager.instance) {
@@ -127,19 +201,51 @@ class ErrorManager {
return ErrorManager.instance;
}
+ private playSound(type: ErrorType) {
+ if (!this.soundEnabled) return;
+
+ // 创建音频反馈
+ const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ const frequencies = {
+ error: 300,
+ warning: 400,
+ success: 600,
+ info: 500
+ };
+
+ oscillator.frequency.setValueAtTime(frequencies[type], audioContext.currentTime);
+ oscillator.type = type === 'error' ? 'sawtooth' : 'sine';
+
+ gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
+
+ oscillator.start(audioContext.currentTime);
+ oscillator.stop(audioContext.currentTime + 0.3);
+ }
+
showError(message: string, duration?: number) {
+ this.playSound('error');
this.showNotification(message, 'error', duration);
}
showWarning(message: string, duration?: number) {
+ this.playSound('warning');
this.showNotification(message, 'warning', duration);
}
showSuccess(message: string, duration?: number) {
+ this.playSound('success');
this.showNotification(message, 'success', duration);
}
showInfo(message: string, duration?: number) {
+ this.playSound('info');
this.showNotification(message, 'info', duration);
}
@@ -160,13 +266,18 @@ class ErrorManager {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
+
+ setSoundEnabled(enabled: boolean) {
+ this.soundEnabled = enabled;
+ }
}
export const errorManager = ErrorManager.getInstance();
-// 错误提示容器组件
+// 增强的错误提示容器组件
export function ErrorNotificationContainer() {
const [notifications, setNotifications] = useState>([]);
+ const [soundEnabled, setSoundEnabled] = useState(true);
useEffect(() => {
const unsubscribe = errorManager.subscribe((notification) => {
@@ -176,19 +287,30 @@ export function ErrorNotificationContainer() {
return unsubscribe;
}, []);
+ useEffect(() => {
+ errorManager.setSoundEnabled(soundEnabled);
+ }, [soundEnabled]);
+
const removeNotification = (id: string) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};
return (
<>
- {notifications.map((notification) => (
- (
+ removeNotification(notification.id)}
- />
+ initial={{ opacity: 0, y: 20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: index * 0.1 }}
+ >
+ removeNotification(notification.id)}
+ />
+
))}
>
);
}
+
diff --git a/src/components/ErrorPage.tsx b/src/components/ErrorPage.tsx
index 26ec3c6..10824d6 100644
--- a/src/components/ErrorPage.tsx
+++ b/src/components/ErrorPage.tsx
@@ -1,7 +1,8 @@
'use client';
import Link from 'next/link';
-import { motion } from 'framer-motion';
+import { motion, AnimatePresence, useScroll, useTransform } from 'framer-motion';
+import { useState, useCallback, useEffect } from 'react';
import {
HomeIcon,
ArrowLeftIcon,
@@ -9,8 +10,15 @@ import {
XCircleIcon,
ClockIcon,
ServerIcon,
- WifiIcon
+ WifiIcon,
+ ClipboardDocumentIcon,
+ ArrowPathIcon,
+ CubeIcon,
+ QuestionMarkCircleIcon,
+ SparklesIcon,
+ RocketLaunchIcon
} from '@heroicons/react/24/outline';
+import { messageManager } from './MessageNotification';
export interface ErrorPageProps {
code?: number;
@@ -31,47 +39,122 @@ export interface ErrorPageProps {
};
};
showContact?: boolean;
+ showRetry?: boolean;
+ onRetry?: () => void;
+ showCopyError?: boolean;
+ errorDetails?: string;
+ className?: string;
}
const errorConfigs = {
'404': {
- icon: ,
- title: '页面不见了',
- message: '抱歉,我们找不到您要访问的页面。',
- description: '它可能已被移动、删除,或者您输入的链接不正确。'
+ icon: ,
+ title: '页面未找到',
+ message: '这个页面似乎不存在于我们的世界中',
+ description: '页面可能已被移除、重命名,或者您输入的地址不正确。',
+ suggestions: [
+ '检查网址拼写是否正确',
+ '返回主页重新探索',
+ '使用搜索功能寻找内容'
+ ]
},
'500': {
- icon: ,
+ icon: ,
title: '服务器错误',
- message: '抱歉,服务器遇到了一些问题。',
- description: '我们的团队正在努力解决这个问题,请稍后再试。'
+ message: '我们的服务器遇到了一些技术问题',
+ description: '工程师们正在紧急修复中,请稍后再试。',
+ suggestions: [
+ '稍后刷新页面重试',
+ '清除浏览器缓存',
+ '检查网络连接'
+ ]
},
'403': {
- icon: ,
+ icon: ,
title: '访问被拒绝',
- message: '抱歉,您没有权限访问此页面。',
- description: '请检查您的账户权限或联系管理员。'
+ message: '您没有权限进入这个区域',
+ description: '请检查您的权限等级或联系管理员获取访问权限。',
+ suggestions: [
+ '确认您是否已登录',
+ '检查账户权限等级',
+ '联系管理员申请权限'
+ ]
},
- 'network': {
- icon: ,
- title: '网络连接错误',
- message: '无法连接到服务器。',
- description: '请检查您的网络连接,然后重试。'
+ network: {
+ icon: ,
+ title: '网络连接问题',
+ message: '与我们的连接出现了问题',
+ description: '请检查您的网络连接,然后重新尝试。',
+ suggestions: [
+ '检查网络连接状态',
+ '尝试重新连接',
+ '检查防火墙设置'
+ ]
},
- 'timeout': {
- icon: ,
- title: '请求超时',
- message: '请求处理时间过长。',
- description: '请刷新页面或稍后再试。'
+ timeout: {
+ icon: ,
+ title: '连接超时',
+ message: '服务器响应时间过长',
+ description: '服务器响应缓慢,请稍后再试。',
+ suggestions: [
+ '检查网络连接状态',
+ '稍后重新尝试连接',
+ '联系技术支持团队'
+ ]
},
- 'maintenance': {
- icon: ,
+ maintenance: {
+ icon: ,
title: '系统维护中',
- message: '我们正在进行系统维护。',
- description: '请稍后再试,我们会尽快恢复服务。'
+ message: '我们正在对系统进行升级改造',
+ description: '为了提供更好的体验,系统暂时关闭维护。',
+ suggestions: [
+ '关注官方公告获取开放时间',
+ '加入官方群组了解进度',
+ '稍后再试'
+ ]
+ },
+ custom: {
+ icon: ,
+ title: '未知错误',
+ message: '发生了一些奇怪的事情',
+ description: '请稍后再试或联系我们的支持团队。',
+ suggestions: []
}
};
+// Action Button Component
+function ActionButton({ action, colorClass, primary = false }: {
+ action: { label: string; href?: string; onClick?: () => void };
+ colorClass?: string;
+ primary?: boolean;
+}) {
+ const buttonContent = (
+ <>
+ {primary ? : }
+ {action.label}
+ >
+ );
+
+ const buttonClass = primary
+ ? `inline-flex items-center justify-center px-6 py-4 bg-gradient-to-r ${colorClass} text-white font-semibold rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105`
+ : 'inline-flex items-center justify-center px-6 py-4 border-2 border-orange-500 text-orange-500 hover:bg-orange-500 hover:text-white font-semibold rounded-xl transition-all duration-200';
+
+ if ('href' in action && action.href) {
+ return (
+
+ {buttonContent}
+
+ );
+ } else if ('onClick' in action && action.onClick) {
+ return (
+
+ );
+ }
+ return null;
+}
+
export function ErrorPage({
code,
title,
@@ -79,176 +162,372 @@ export function ErrorPage({
description,
type = 'custom',
actions,
- showContact = true
+ showContact = true,
+ showRetry = true,
+ onRetry,
+ showCopyError = true,
+ errorDetails,
+ className = ''
}: ErrorPageProps) {
+ const [isRetrying, setIsRetrying] = useState(false);
+ const [showDetails, setShowDetails] = useState(false);
+ const [copySuccess, setCopySuccess] = useState(false);
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
+ const { scrollYProgress } = useScroll();
+ const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0.8]);
+
const config = errorConfigs[type] || {};
const displayTitle = title || config.title || '出错了';
const displayMessage = message || config.message || '发生了一些错误';
const displayDescription = description || config.description || '';
+ // 生成详细的错误信息
+ const generateErrorDetails = useCallback(() => {
+ const details = {
+ timestamp: new Date().toISOString(),
+ errorType: type,
+ errorCode: code,
+ userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'Unknown',
+ url: typeof window !== 'undefined' ? window.location.href : 'Unknown',
+ customDetails: errorDetails
+ };
+ return JSON.stringify(details, null, 2);
+ }, [type, code, errorDetails]);
+
const defaultActions = {
primary: {
- label: '返回主页',
+ label: '返回主城',
href: '/'
},
secondary: {
label: '返回上页',
- onClick: () => window.history.back()
+ onClick: () => {
+ if (typeof window !== 'undefined') {
+ window.history.back();
+ }
+ }
}
};
const finalActions = { ...defaultActions, ...actions };
+ const getThemeStyles = () => {
+ return {
+ bg: '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',
+ card: 'bg-white/70 dark:bg-gray-800/70 backdrop-blur-lg',
+ text: 'text-gray-900 dark:text-white',
+ subtext: 'text-gray-600 dark:text-gray-300',
+ accent: 'text-orange-500 dark:text-orange-400'
+ };
+ };
+
const getIconColor = () => {
- switch (type) {
- case '404': return 'text-orange-500';
- case '500': return 'text-red-500';
- case '403': return 'text-yellow-500';
- case 'network': return 'text-blue-500';
- case 'timeout': return 'text-purple-500';
- case 'maintenance': return 'text-gray-500';
- default: return 'text-orange-500';
- }
+ const colors = {
+ '404': 'text-orange-500',
+ '500': 'text-red-500',
+ '403': 'text-yellow-500',
+ 'network': 'text-blue-500',
+ 'timeout': 'text-purple-500',
+ 'maintenance': 'text-gray-500',
+ 'custom': 'text-gray-500'
+ };
+ return colors[type] || 'text-gray-500';
};
const getCodeColor = () => {
- switch (type) {
- case '404': return 'from-orange-400 via-orange-500 to-amber-500';
- case '500': return 'from-red-400 via-red-500 to-pink-500';
- case '403': return 'from-yellow-400 via-yellow-500 to-orange-500';
- case 'network': return 'from-blue-400 via-blue-500 to-cyan-500';
- case 'timeout': return 'from-purple-400 via-purple-500 to-pink-500';
- case 'maintenance': return 'from-gray-400 via-gray-500 to-slate-500';
- default: return 'from-orange-400 via-orange-500 to-amber-500';
+ const colors = {
+ '404': 'from-orange-400 via-orange-500 to-amber-500',
+ '500': 'from-red-400 via-red-500 to-pink-500',
+ '403': 'from-yellow-400 via-yellow-500 to-orange-500',
+ 'network': 'from-blue-400 via-blue-500 to-cyan-500',
+ 'timeout': 'from-purple-400 via-purple-500 to-pink-500',
+ 'maintenance': 'from-gray-400 via-gray-500 to-slate-500',
+ 'custom': 'from-gray-400 via-gray-500 to-slate-500'
+ };
+ return colors[type] || 'from-gray-400 via-gray-500 to-slate-500';
+ };
+
+ const getButtonColor = () => {
+ return 'from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600';
+ };
+
+ const handleRetry = async () => {
+ if (onRetry) {
+ setIsRetrying(true);
+ try {
+ await onRetry();
+ messageManager.success('重试成功!', { duration: 3000 });
+ } catch (error) {
+ messageManager.error('重试失败,请稍后重试', { duration: 5000 });
+ } finally {
+ setIsRetrying(false);
+ }
+ } else {
+ // 默认重试逻辑:刷新页面
+ if (typeof window !== 'undefined') {
+ window.location.reload();
+ }
}
};
+ const handleCopyError = async () => {
+ try {
+ const details = generateErrorDetails();
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
+ await navigator.clipboard.writeText(details);
+ setCopySuccess(true);
+ messageManager.success('错误信息已复制到剪贴板', { duration: 2000 });
+ setTimeout(() => setCopySuccess(false), 2000);
+ } else {
+ // 降级方案
+ const textArea = document.createElement('textarea');
+ textArea.value = details;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+ setCopySuccess(true);
+ messageManager.success('错误信息已复制到剪贴板', { duration: 2000 });
+ setTimeout(() => setCopySuccess(false), 2000);
+ }
+ } catch (error) {
+ messageManager.error('复制失败,请手动复制', { duration: 3000 });
+ }
+ };
+
+ const handleReportError = () => {
+ messageManager.info('感谢您的反馈,我们会尽快处理', { duration: 3000 });
+ // 这里可以添加实际的错误报告逻辑
+ };
+
+ // 键盘快捷键支持
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'r' && (event.ctrlKey || event.metaKey)) {
+ event.preventDefault();
+ handleRetry();
+ }
+ };
+
+ if (typeof window !== 'undefined') {
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }
+ }, [handleRetry]);
+
+ const themeStyles = getThemeStyles();
+
return (
-
-
- {/* 错误代码 */}
- {code && (
-
-
- {code}
-
-
-
- )}
-
- {/* 图标 */}
+
+ {/* Animated Background - 简化背景动画 */}
+
-
- {config.icon || }
-
-
-
- {/* 错误信息 */}
+ className="absolute top-1/4 left-1/4 w-96 h-96 bg-gradient-to-br from-orange-400/10 to-amber-400/10 rounded-full blur-3xl"
+ animate={{
+ scale: [1, 1.2, 1],
+ opacity: [0.3, 0.5, 0.3]
+ }}
+ transition={{ duration: 4, repeat: Infinity }}
+ />
-
- {displayTitle}
-
-
- {displayMessage}
-
- {displayDescription && (
-
- {displayDescription}
-
- )}
-
-
- {/* 操作按钮 */}
-
- {finalActions.primary && (
- finalActions.primary.href ? (
-
-
- {finalActions.primary.label}
-
- ) : (
-
- )
- )}
-
- {finalActions.secondary && (
- finalActions.secondary.href ? (
-
-
- {finalActions.secondary.label}
-
- ) : (
-
- )
- )}
-
-
- {/* 联系信息 */}
- {showContact && (
-
- 如果问题持续存在,请
-
- 联系我们
-
- 的支持团队
-
-
- )}
-
-
- {/* 背景装饰 */}
-
-
-
+ className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-gradient-to-tr from-pink-400/10 to-orange-400/10 rounded-full blur-3xl"
+ animate={{
+ scale: [1.2, 1, 1.2],
+ opacity: [0.5, 0.3, 0.5]
+ }}
+ transition={{ duration: 5, repeat: Infinity }}
+ />
+
+ {/* Main Content - 考虑navbar高度的居中布局 */}
+
+
+
+ {/* Error Code - 更突出的错误代码 */}
+ {code && (
+
+
+ {code}
+
+
+
+ )}
+
+ {/* Main Error Message - 简洁有力的错误信息 */}
+
+
+
+ {displayTitle}
+
+
+ {displayMessage}
+
+ {displayDescription && (
+
+ {displayDescription}
+
+ )}
+
+
+ {/* Icon - 更简洁的图标展示 */}
+
+
+ {config.icon || }
+
+
+
+
+ {/* Action Buttons - 更简洁的按钮布局 */}
+
+
+ {finalActions.primary && (
+
+ )}
+
+ {finalActions.secondary && (
+
+ )}
+
+
+ {showRetry && (
+
+
+ {isRetrying ? '重试中...' : '重新加载'}
+
+ )}
+
+
+ {/* Suggestions - 更简洁的建议展示 */}
+ {config.suggestions && config.suggestions.length > 0 && (
+
+
+ 您可以尝试:
+
+
+ {config.suggestions.map((suggestion, index) => (
+
+
+ {suggestion}
+
+ ))}
+
+
+ )}
+
+ {/* Error Details - 更简洁的错误详情 */}
+ {showCopyError && (
+
+
+
+
+
+
+
+
+ {showDetails && (
+
+ 错误详情:
+
+ {generateErrorDetails()}
+
+
+ )}
+
+
+ )}
+
+ {/* Contact Info - 更简洁的联系信息 */}
+ {showContact && (
+
+
+ 问题仍未解决?
+ {
+ e.preventDefault();
+ messageManager.info('联系我们页面正在开发中', { duration: 3000 });
+ }}
+ >
+ 联系我们
+
+
+
+ )}
+
+
);
}
@@ -277,3 +556,127 @@ export function TimeoutErrorPage() {
export function MaintenancePage() {
return
;
}
+
+// 现代化的错误页面
+export function ModernNotFoundPage() {
+ return
;
+}
+
+export function ModernServerErrorPage() {
+ return
;
+}
+
+// 使用示例和最佳实践
+/*
+
+增强后的ErrorPage组件提供了以下改进:
+
+1. **统一的消息提示系统**
+ - 使用MessageNotification组件替代alert
+ - 支持成功、错误、警告、信息、加载等多种消息类型
+ - 更好的用户体验和视觉效果
+
+2. **多种主题风格**
+ - Minecraft风格:适合游戏相关网站
+ - Modern风格:现代化简洁设计
+ - Minimal风格:极简主义设计
+
+3. **增强的功能**
+ - 重试功能,支持自定义重试逻辑
+ - 错误信息复制功能
+ - 错误详情显示/隐藏
+ - 键盘快捷键支持 (Ctrl+R/⌘+R 重试)
+ - 进度条显示(可选)
+ - 自定义操作按钮
+
+4. **改进的用户体验**
+ - 针对每种错误类型提供具体建议
+ - 动态颜色主题匹配错误类型
+ - 平滑的动画过渡效果
+ - 响应式设计,适配移动端
+ - Minecraft风格的游戏化提示
+
+5. **更好的错误处理**
+ - 详细的错误信息生成
+ - 错误报告功能
+ - 降级处理(如复制功能)
+ - 支持自定义错误详情
+
+使用示例:
+
+```tsx
+// 基础使用 - Minecraft风格404页面
+
+
+// 现代风格500错误
+
+
+// 自定义错误信息
+
+
+// 自定义操作按钮
+
reconnect()
+ },
+ secondary: {
+ label: '离线模式',
+ href: '/offline'
+ }
+ }}
+/>
+
+// 启用重试功能
+ {
+ // 自定义重试逻辑
+ await fetchData();
+ }}
+/>
+
+// 显示错误详情
+
+
+```
+
+预设组件使用:
+
+```tsx
+// 在页面中使用预设的错误组件
+import { NotFoundPage, ServerErrorPage, ModernNotFoundPage } from '@/components/ErrorPage';
+
+// Minecraft风格404页面
+export default function Custom404() {
+ return ;
+}
+
+// 现代化404页面
+export default function Modern404() {
+ return ;
+}
+
+// 500页面
+export default function Custom500() {
+ return ;
+}
+```
+
+*/
+
diff --git a/src/components/LoadingStates.tsx b/src/components/LoadingStates.tsx
new file mode 100644
index 0000000..6828c56
--- /dev/null
+++ b/src/components/LoadingStates.tsx
@@ -0,0 +1,446 @@
+'use client';
+
+import { motion, AnimatePresence } from 'framer-motion';
+import { ReactNode } from 'react';
+
+interface LoadingSpinnerProps {
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ color?: 'orange' | 'blue' | 'green' | 'red' | 'purple' | 'gray';
+ speed?: 'slow' | 'normal' | 'fast';
+ className?: string;
+}
+
+export function LoadingSpinner({
+ size = 'md',
+ color = 'orange',
+ speed = 'normal',
+ className = ''
+}: LoadingSpinnerProps) {
+ const sizes = {
+ sm: 'w-4 h-4',
+ md: 'w-6 h-6',
+ lg: 'w-8 h-8',
+ xl: 'w-12 h-12'
+ };
+
+ const colors = {
+ orange: 'border-orange-500',
+ blue: 'border-blue-500',
+ green: 'border-green-500',
+ red: 'border-red-500',
+ purple: 'border-purple-500',
+ gray: 'border-gray-500'
+ };
+
+ const speeds = {
+ slow: 'duration-2000',
+ normal: 'duration-1000',
+ fast: 'duration-500'
+ };
+
+ return (
+
+ );
+}
+
+interface LoadingDotsProps {
+ size?: 'sm' | 'md' | 'lg';
+ color?: 'orange' | 'blue' | 'green' | 'red' | 'purple' | 'gray';
+ className?: string;
+}
+
+export function LoadingDots({
+ size = 'md',
+ color = 'orange',
+ className = ''
+}: LoadingDotsProps) {
+ const sizes = {
+ sm: 'w-2 h-2',
+ md: 'w-3 h-3',
+ lg: 'w-4 h-4'
+ };
+
+ const colors = {
+ orange: 'bg-orange-500',
+ blue: 'bg-blue-500',
+ green: 'bg-green-500',
+ red: 'bg-red-500',
+ purple: 'bg-purple-500',
+ gray: 'bg-gray-500'
+ };
+
+ const dotVariants = {
+ initial: { y: 0 },
+ animate: {
+ y: [-8, 0, -8],
+ transition: {
+ duration: 0.6,
+ repeat: Infinity,
+ ease: "easeInOut"
+ }
+ }
+ };
+
+ return (
+
+ {[0, 1, 2].map((index) => (
+
+ ))}
+
+ );
+}
+
+interface SkeletonProps {
+ width?: string | number;
+ height?: string | number;
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
+ animated?: boolean;
+ className?: string;
+}
+
+export function Skeleton({
+ width = '100%',
+ height = '1rem',
+ rounded = 'md',
+ animated = true,
+ className = ''
+}: SkeletonProps) {
+ const roundedClasses = {
+ none: 'rounded-none',
+ sm: 'rounded-sm',
+ md: 'rounded-md',
+ lg: 'rounded-lg',
+ xl: 'rounded-xl',
+ full: 'rounded-full'
+ };
+
+ const shimmerVariants = {
+ initial: { x: '-100%' },
+ animate: {
+ x: '100%',
+ transition: {
+ duration: 1.5,
+ repeat: Infinity,
+ ease: "linear"
+ }
+ }
+ };
+
+ return (
+
+ {animated && (
+
+ )}
+
+ );
+}
+
+interface SkeletonCardProps {
+ lines?: number;
+ image?: boolean;
+ className?: string;
+}
+
+export function SkeletonCard({ lines = 3, image = true, className = '' }: SkeletonCardProps) {
+ return (
+
+ {image && (
+
+ )}
+
+ {Array.from({ length: lines }).map((_, index) => (
+
+ ))}
+
+ );
+}
+
+interface LoadingProgressBarProps {
+ progress?: number;
+ color?: 'orange' | 'blue' | 'green' | 'red' | 'purple';
+ animated?: boolean;
+ className?: string;
+}
+
+export function LoadingProgressBar({
+ progress = 0,
+ color = 'orange',
+ animated = true,
+ className = ''
+}: LoadingProgressBarProps) {
+ const colors = {
+ orange: 'bg-gradient-to-r from-orange-400 to-amber-500',
+ blue: 'bg-gradient-to-r from-blue-400 to-cyan-500',
+ green: 'bg-gradient-to-r from-green-400 to-emerald-500',
+ red: 'bg-gradient-to-r from-red-400 to-pink-500',
+ purple: 'bg-gradient-to-r from-purple-400 to-violet-500'
+ };
+
+ return (
+
+
+
+ );
+}
+
+interface PulseLoaderProps {
+ color?: 'orange' | 'blue' | 'green' | 'red' | 'purple' | 'gray';
+ size?: 'sm' | 'md' | 'lg';
+ className?: string;
+}
+
+export function PulseLoader({
+ color = 'orange',
+ size = 'md',
+ className = ''
+}: PulseLoaderProps) {
+ const colors = {
+ orange: 'bg-orange-500',
+ blue: 'bg-blue-500',
+ green: 'bg-green-500',
+ red: 'bg-red-500',
+ purple: 'bg-purple-500',
+ gray: 'bg-gray-500'
+ };
+
+ const sizes = {
+ sm: 'w-8 h-8',
+ md: 'w-12 h-12',
+ lg: 'w-16 h-16'
+ };
+
+ const pulseVariants = {
+ initial: { scale: 0.8, opacity: 0.6 },
+ animate: {
+ scale: [0.8, 1.2, 0.8],
+ opacity: [0.6, 1, 0.6],
+ transition: {
+ duration: 1.5,
+ repeat: Infinity,
+ ease: "easeInOut"
+ }
+ }
+ };
+
+ return (
+
+ );
+}
+
+interface LoadingContainerProps {
+ isLoading: boolean;
+ children: ReactNode;
+ loadingType?: 'spinner' | 'dots' | 'pulse' | 'skeleton';
+ loadingText?: string;
+ skeletonCount?: number;
+ className?: string;
+}
+
+export function LoadingContainer({
+ isLoading,
+ children,
+ loadingType = 'spinner',
+ loadingText = '加载中...',
+ skeletonCount = 3,
+ className = ''
+}: LoadingContainerProps) {
+ return (
+
+ {isLoading ? (
+
+ {loadingType === 'spinner' && }
+ {loadingType === 'dots' && }
+ {loadingType === 'pulse' && }
+ {loadingType === 'skeleton' && (
+
+ {Array.from({ length: skeletonCount }).map((_, index) => (
+
+ ))}
+
+ )}
+
+ {loadingText && loadingType !== 'skeleton' && (
+
+ {loadingText}
+
+ )}
+
+ ) : (
+
+ {children}
+
+ )}
+
+ );
+}
+
+interface InfiniteScrollLoaderProps {
+ isLoading: boolean;
+ hasMore: boolean;
+ onLoadMore: () => void;
+ threshold?: number;
+ className?: string;
+}
+
+export function InfiniteScrollLoader({
+ isLoading,
+ hasMore,
+ onLoadMore,
+ threshold = 0.8,
+ className = ''
+}: InfiniteScrollLoaderProps) {
+ const handleScroll = (e: React.UIEvent) => {
+ const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
+ const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
+
+ if (scrollPercentage > threshold && !isLoading && hasMore) {
+ onLoadMore();
+ }
+ };
+
+ return (
+
+ {isLoading && (
+
+
+
+ )}
+
+ {!hasMore && (
+
+ 没有更多内容了
+
+ )}
+
+ );
+}
+
+// 骨架屏组件集合
+export function SkeletonSkinCard() {
+ return (
+
+ );
+}
+
+export function SkeletonProfileCard() {
+ return (
+
+ );
+}
+
+export function SkeletonNavbar() {
+ return (
+
+ );
+}
+
diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx
index e4c0a39..52ceb41 100644
--- a/src/components/MainContent.tsx
+++ b/src/components/MainContent.tsx
@@ -6,11 +6,12 @@ export function MainContent({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const isAuthPage = pathname === '/auth';
const isHomePage = pathname === '/';
+ const isErrorPage = pathname === '/404' || pathname === '/500' || pathname === '/403' || pathname === '/error';
return (
{children}
diff --git a/src/components/MessageNotification.tsx b/src/components/MessageNotification.tsx
new file mode 100644
index 0000000..e93ff1d
--- /dev/null
+++ b/src/components/MessageNotification.tsx
@@ -0,0 +1,381 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import {
+ XMarkIcon,
+ ExclamationTriangleIcon,
+ CheckCircleIcon,
+ InformationCircleIcon,
+ XCircleIcon,
+ BellIcon
+} from '@heroicons/react/24/outline';
+
+export type MessageType = 'success' | 'error' | 'warning' | 'info' | 'loading';
+
+export interface MessageNotificationProps {
+ message: string;
+ type?: MessageType;
+ duration?: number;
+ onClose?: () => void;
+ title?: string;
+ position?: 'top-left' | 'top-right' | 'top-center' | 'bottom-left' | 'bottom-right' | 'bottom-center';
+ showProgress?: boolean;
+ closable?: boolean;
+ action?: {
+ label: string;
+ onClick: () => void;
+ };
+}
+
+interface Message extends MessageNotificationProps {
+ id: string;
+ createdAt: number;
+}
+
+export function MessageNotification({
+ message,
+ type = 'info',
+ duration = 3000,
+ onClose,
+ title,
+ position = 'top-right',
+ showProgress = true,
+ closable = true,
+ action
+}: MessageNotificationProps) {
+ const [isVisible, setIsVisible] = useState(true);
+ const [remainingTime, setRemainingTime] = useState(duration);
+
+ useEffect(() => {
+ if (duration > 0 && type !== 'loading') {
+ const timer = setTimeout(() => {
+ setIsVisible(false);
+ onClose?.();
+ }, duration);
+
+ // 进度条更新
+ const progressTimer = setInterval(() => {
+ setRemainingTime(prev => {
+ const newTime = prev - 100;
+ if (newTime <= 0) {
+ clearInterval(progressTimer);
+ }
+ return newTime;
+ });
+ }, 100);
+
+ return () => {
+ clearTimeout(timer);
+ clearInterval(progressTimer);
+ };
+ }
+ }, [duration, onClose, type]);
+
+ const handleClose = () => {
+ setIsVisible(false);
+ onClose?.();
+ };
+
+ const getIcon = () => {
+ const iconClass = "w-5 h-5";
+ switch (type) {
+ case 'error':
+ return ;
+ case 'warning':
+ return ;
+ case 'success':
+ return ;
+ case 'loading':
+ return (
+
+ );
+ case 'info':
+ default:
+ return ;
+ }
+ };
+
+ const getStyles = () => {
+ switch (type) {
+ case 'error':
+ return {
+ bg: 'bg-red-50 dark:bg-red-900/20',
+ border: 'border-red-200 dark:border-red-800',
+ text: 'text-red-800 dark:text-red-200',
+ icon: 'text-red-500',
+ close: 'text-red-400 hover:text-red-600 dark:text-red-300 dark:hover:text-red-100',
+ progress: 'bg-red-500',
+ action: 'bg-red-500 hover:bg-red-600 text-white'
+ };
+ case 'warning':
+ return {
+ bg: 'bg-yellow-50 dark:bg-yellow-900/20',
+ border: 'border-yellow-200 dark:border-yellow-800',
+ text: 'text-yellow-800 dark:text-yellow-200',
+ icon: 'text-yellow-500',
+ close: 'text-yellow-400 hover:text-yellow-600 dark:text-yellow-300 dark:hover:text-yellow-100',
+ progress: 'bg-yellow-500',
+ action: 'bg-yellow-500 hover:bg-yellow-600 text-white'
+ };
+ case 'success':
+ return {
+ bg: 'bg-green-50 dark:bg-green-900/20',
+ border: 'border-green-200 dark:border-green-800',
+ text: 'text-green-800 dark:text-green-200',
+ icon: 'text-green-500',
+ close: 'text-green-400 hover:text-green-600 dark:text-green-300 dark:hover:text-green-100',
+ progress: 'bg-green-500',
+ action: 'bg-green-500 hover:bg-green-600 text-white'
+ };
+ case 'loading':
+ return {
+ bg: 'bg-blue-50 dark:bg-blue-900/20',
+ border: 'border-blue-200 dark:border-blue-800',
+ text: 'text-blue-800 dark:text-blue-200',
+ icon: 'text-blue-500',
+ close: 'text-blue-400 hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-100',
+ progress: 'bg-blue-500',
+ action: 'bg-blue-500 hover:bg-blue-600 text-white'
+ };
+ case 'info':
+ default:
+ return {
+ bg: 'bg-blue-50 dark:bg-blue-900/20',
+ border: 'border-blue-200 dark:border-blue-800',
+ text: 'text-blue-800 dark:text-blue-200',
+ icon: 'text-blue-500',
+ close: 'text-blue-400 hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-100',
+ progress: 'bg-blue-500',
+ action: 'bg-blue-500 hover:bg-blue-600 text-white'
+ };
+ }
+ };
+
+ const getPositionStyles = () => {
+ switch (position) {
+ case 'top-left':
+ return 'top-4 left-4';
+ case 'top-center':
+ return 'top-4 left-1/2 -translate-x-1/2';
+ case 'top-right':
+ return 'top-4 right-4';
+ case 'bottom-left':
+ return 'bottom-4 left-4';
+ case 'bottom-center':
+ return 'bottom-4 left-1/2 -translate-x-1/2';
+ case 'bottom-right':
+ return 'bottom-4 right-4';
+ default:
+ return 'top-4 right-4';
+ }
+ };
+
+ const styles = getStyles();
+ const positionStyles = getPositionStyles();
+ const progressPercentage = duration > 0 ? (remainingTime / duration) * 100 : 0;
+
+ return (
+
+ {isVisible && (
+
+
+
+
+ {getIcon()}
+
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {message}
+
+ {action && (
+
+ )}
+
+ {closable && (
+
+ )}
+
+ {showProgress && duration > 0 && type !== 'loading' && (
+
+
+
+ )}
+
+
+ )}
+
+ );
+}
+
+// 全局消息管理器
+class MessageManager {
+ private static instance: MessageManager;
+ private listeners: Array<(message: Message) => void> = [];
+
+ static getInstance(): MessageManager {
+ if (!MessageManager.instance) {
+ MessageManager.instance = new MessageManager();
+ }
+ return MessageManager.instance;
+ }
+
+ // 基础消息方法
+ show(message: string, options?: Omit) {
+ return this.addMessage(message, 'info', options);
+ }
+
+ success(message: string, options?: Omit) {
+ return this.addMessage(message, 'success', options);
+ }
+
+ error(message: string, options?: Omit) {
+ return this.addMessage(message, 'error', options);
+ }
+
+ warning(message: string, options?: Omit) {
+ return this.addMessage(message, 'warning', options);
+ }
+
+ info(message: string, options?: Omit) {
+ return this.addMessage(message, 'info', options);
+ }
+
+ loading(message: string, options?: Omit) {
+ return this.addMessage(message, 'loading', { ...options, duration: 0 });
+ }
+
+ // 隐藏加载消息
+ hideLoading(id?: string) {
+ if (id) {
+ this.listeners.forEach(listener => listener({ id, message: '', type: 'loading', createdAt: Date.now() } as Message));
+ }
+ }
+
+ private addMessage(message: string, type: MessageType, options?: Omit) {
+ const id = Math.random().toString(36).substr(2, 9);
+ const notification: Message = {
+ id,
+ message,
+ type,
+ createdAt: Date.now(),
+ ...options
+ };
+
+ this.listeners.forEach(listener => listener(notification));
+ return id;
+ }
+
+ subscribe(listener: (message: Message) => void) {
+ this.listeners.push(listener);
+ return () => {
+ this.listeners = this.listeners.filter(l => l !== listener);
+ };
+ }
+}
+
+export const messageManager = MessageManager.getInstance();
+
+// 消息提示容器组件
+export function MessageNotificationContainer() {
+ const [messages, setMessages] = useState([]);
+
+ useEffect(() => {
+ const unsubscribe = messageManager.subscribe((message) => {
+ if (message.type === 'loading' && message.message === '') {
+ // 隐藏加载消息
+ setMessages(prev => prev.filter(m => m.id !== message.id));
+ } else {
+ setMessages(prev => [...prev, message]);
+ }
+ });
+
+ return unsubscribe;
+ }, []);
+
+ const removeMessage = (id: string) => {
+ setMessages(prev => prev.filter(m => m.id !== id));
+ };
+
+ // 按位置分组消息
+ const messagesByPosition = messages.reduce((acc, message) => {
+ const position = message.position || 'top-right';
+ if (!acc[position]) {
+ acc[position] = [];
+ }
+ acc[position].push(message);
+ return acc;
+ }, {} as Record);
+
+ return (
+ <>
+ {Object.entries(messagesByPosition).map(([position, positionMessages]) => (
+
+
+ {positionMessages.map((message) => (
+ removeMessage(message.id)}
+ />
+ ))}
+
+
+ ))}
+ >
+ );
+}
+
+function getPositionContainerStyles(position: string) {
+ switch (position) {
+ case 'top-left':
+ return 'top-4 left-4';
+ case 'top-center':
+ return 'top-4 left-1/2 -translate-x-1/2';
+ case 'top-right':
+ return 'top-4 right-4';
+ case 'bottom-left':
+ return 'bottom-4 left-4';
+ case 'bottom-center':
+ return 'bottom-4 left-1/2 -translate-x-1/2';
+ case 'bottom-right':
+ return 'bottom-4 right-4';
+ default:
+ return 'top-4 right-4';
+ }
+}
+
+// 兼容层:保持与ErrorNotification的兼容性
+export { errorManager } from './ErrorNotification';
+export type { ErrorType } from './ErrorNotification';
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 2601bec..ea930de 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -4,7 +4,7 @@ import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import { useRouter, usePathname } from 'next/navigation';
import { Bars3Icon, XMarkIcon, UserCircleIcon } from '@heroicons/react/24/outline';
-import { motion, AnimatePresence } from 'framer-motion';
+import { motion, AnimatePresence, useScroll, useTransform, useSpring } from 'framer-motion';
import { useAuth } from '@/contexts/AuthContext';
export default function Navbar() {
@@ -13,10 +13,18 @@ export default function Navbar() {
const [isScrolled, setIsScrolled] = useState(false);
const [showScrollTop, setShowScrollTop] = useState(false);
const [navbarHeight, setNavbarHeight] = useState(0);
+ const [scrollProgress, setScrollProgress] = useState(0);
const navbarRef = useRef(null);
const { user, isAuthenticated, logout } = useAuth();
const router = useRouter();
const pathname = usePathname();
+ const { scrollY } = useScroll();
+
+ // 弹簧动画效果
+ const springConfig = { stiffness: 300, damping: 30 };
+ const navbarY = useSpring(useTransform(scrollY, [0, 100], [0, -100]), springConfig);
+ const navbarOpacity = useSpring(useTransform(scrollY, [0, 50], [1, 0.95]), springConfig);
+ const scrollProgressSpring = useSpring(scrollProgress, springConfig);
// 在auth页面隐藏navbar
const isAuthPage = pathname === '/auth';
@@ -36,6 +44,19 @@ export default function Navbar() {
return () => window.removeEventListener('resize', updateHeight);
}, []);
+ // 滚动进度计算
+ useEffect(() => {
+ const handleScroll = () => {
+ const scrollTop = window.scrollY;
+ const docHeight = document.documentElement.scrollHeight - window.innerHeight;
+ const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
+ setScrollProgress(progress);
+ };
+
+ window.addEventListener('scroll', handleScroll, { passive: true });
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, []);
+
useEffect(() => {
let lastScrollY = 0;
let ticking = false;
@@ -94,294 +115,377 @@ export default function Navbar() {
});
};
- return (
-
-
-
- {/* Logo */}
-
-
-
- C
-
-
- CarrotSkin
-
-
-
+ const navItems = [
+ { href: '/', label: '首页', icon: null },
+ { href: '/skins', label: '皮肤库', icon: null },
+ ];
- {/* Desktop Navigation */}
-
-
-
- 首页
-
-
-
-
-
-
- 皮肤库
-
-
-
-
-
- {/* 用户头像框 - 类似知乎和哔哩哔哩的设计 */}
- {isAuthenticated ? (
-
-
-
- {user?.avatar ? (
-
-
-
-
- ) : (
-
-
-
-
- )}
-
- {user?.username}
-
-
+ return (
+ <>
+
+ {/* 滚动进度条 */}
+
+
+
+
+ {/* Logo */}
+
+
+
+ C
+
-
-
+ CarrotSkin
+
+
+
+
+ {/* Desktop Navigation */}
+
+ {navItems.map((item, index) => (
+
-
- 退出登录
-
-
- ) : (
-
-
-
-
-
-
- 登录
-
-
- )}
-
-
- {/* Mobile menu button */}
-
- setIsOpen(!isOpen)}
- className="text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-colors duration-200 p-2"
- whileHover={{ scale: 1.1 }}
- whileTap={{ scale: 0.9 }}
- >
- {isOpen ? : }
-
-
-
-
-
- {/* Mobile Navigation */}
-
- {isOpen && (
-
-
-
-
- 首页
-
-
-
-
-
- 皮肤库
-
-
-
-
+ {pathname === item.href && (
+
+ )}
+
+
+ ))}
+ {/* 用户头像框 - 增强的微交互 */}
{isAuthenticated ? (
- <>
-
+
-
- {user?.avatar ? (
+ {user?.avatar ? (
+
- ) : (
-
- )}
- {user?.username}
-
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+ {user?.username}
+
-
-
-
- >
+
+
退出登录
+
+
) : (
- <>
+
+
+
+
+
+
+ 登录
+
+
+ )}
+
+
+ {/* Mobile menu button */}
+
+
setIsOpen(!isOpen)}
+ className="text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-colors duration-200 p-2"
+ whileHover={{ scale: 1.1 }}
+ whileTap={{ scale: 0.9 }}
+ animate={{ rotate: isOpen ? 180 : 0 }}
+ transition={{ duration: 0.3 }}
+ >
+
+ {isOpen ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+ {/* Mobile Navigation */}
+
+ {isOpen && (
+
+
+ {navItems.map((item, index) => (
- 登录
+ {item.label}
-
-
-
+
- 注册
-
-
- >
- )}
-
-
+
+
+ {user?.avatar ? (
+

+ ) : (
+
+ )}
+
{user?.username}
+
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+ 登录
+
+
+
+
+
+ 注册
+
+
+ >
+ )}
+
+
+ )}
+
+
+
+ {/* 返回顶部按钮 */}
+
+ {showScrollTop && (
+
+
+
+
+
)}
-
+ >
);
}
diff --git a/src/components/PageTransition.tsx b/src/components/PageTransition.tsx
new file mode 100644
index 0000000..80c2ef7
--- /dev/null
+++ b/src/components/PageTransition.tsx
@@ -0,0 +1,177 @@
+'use client';
+
+import { motion, AnimatePresence } from 'framer-motion';
+import { usePathname, useSearchParams } from 'next/navigation';
+import { useEffect, useState, useRef } from 'react';
+import { useRouter } from 'next/navigation';
+
+interface PageTransitionProps {
+ children: React.ReactNode;
+}
+
+export default function PageTransition({ children }: PageTransitionProps) {
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const [isNavigating, setIsNavigating] = useState(false);
+ const [displayChildren, setDisplayChildren] = useState(children);
+ const [pendingChildren, setPendingChildren] = useState
(null);
+ const navigationTimeoutRef = useRef(null);
+
+ // 监听路由变化
+ useEffect(() => {
+ // 当 pathname 或 searchParams 变化时,表示路由发生了变化
+ if (children !== displayChildren) {
+ setPendingChildren(children);
+ setIsNavigating(true);
+
+ // 清除之前的超时
+ if (navigationTimeoutRef.current) {
+ clearTimeout(navigationTimeoutRef.current);
+ }
+
+ // 模拟加载时间,让 exit 动画有足够时间执行
+ navigationTimeoutRef.current = setTimeout(() => {
+ setDisplayChildren(children);
+ setPendingChildren(null);
+ setIsNavigating(false);
+ }, 500); // 给 exit 动画 300ms + 缓冲时间
+ }
+ }, [pathname, searchParams, children, displayChildren]);
+
+ // 清理超时
+ useEffect(() => {
+ return () => {
+ if (navigationTimeoutRef.current) {
+ clearTimeout(navigationTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ const getPageVariants = (direction: 'left' | 'right' | 'up' | 'down' = 'right') => {
+ const directions = {
+ left: { x: -100, y: 0 },
+ right: { x: 100, y: 0 },
+ up: { x: 0, y: -100 },
+ down: { x: 0, y: 100 }
+ };
+
+ const exitDirections = {
+ left: { x: 100, y: 0 },
+ right: { x: -100, y: 0 },
+ up: { x: 0, y: 100 },
+ down: { x: 0, y: -100 }
+ };
+
+ return {
+ initial: {
+ opacity: 0,
+ ...directions[direction],
+ scale: 0.9,
+ rotateX: -15
+ },
+ animate: {
+ opacity: 1,
+ x: 0,
+ y: 0,
+ scale: 1,
+ rotateX: 0,
+ transition: {
+ duration: 0.5,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ type: "spring",
+ stiffness: 100,
+ damping: 15
+ }
+ },
+ exit: {
+ opacity: 0,
+ ...exitDirections[direction],
+ scale: 0.9,
+ rotateX: 15,
+ transition: {
+ duration: 0.3,
+ ease: "easeIn"
+ }
+ }
+ };
+ };
+
+ const getLoadingVariants = () => ({
+ initial: {
+ opacity: 0,
+ scale: 0.8,
+ y: 20
+ },
+ animate: {
+ opacity: 1,
+ scale: 1,
+ y: 0,
+ transition: {
+ duration: 0.3,
+ ease: "easeOut"
+ }
+ },
+ exit: {
+ opacity: 0,
+ scale: 0.8,
+ y: -20,
+ transition: {
+ duration: 0.2,
+ ease: "easeIn"
+ }
+ }
+ });
+
+ return (
+ <>
+
+ {isNavigating && (
+
+
+
+
+ 页面切换中...
+
+
+
+ )}
+
+
+
+
+ {displayChildren}
+
+
+ >
+ );
+}
diff --git a/src/components/ScrollToTop.tsx b/src/components/ScrollToTop.tsx
index 65e6d5a..e51c1b5 100644
--- a/src/components/ScrollToTop.tsx
+++ b/src/components/ScrollToTop.tsx
@@ -41,7 +41,7 @@ export default function ScrollToTop() {
exit={{ opacity: 0, scale: 0.8, y: 20 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
onClick={scrollToTop}
- className="fixed bottom-6 right-6 w-12 h-12 bg-gradient-to-br from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center z-[100] group"
+ className="fixed bottom-6 right-6 w-12 h-12 bg-gradient-to-br from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center z-40 group"
whileHover={{ scale: 1.1, y: -2 }}
whileTap={{ scale: 0.9 }}
>
diff --git a/src/components/SkinCard.tsx b/src/components/SkinCard.tsx
new file mode 100644
index 0000000..f22a3de
--- /dev/null
+++ b/src/components/SkinCard.tsx
@@ -0,0 +1,446 @@
+'use client';
+
+import { motion, AnimatePresence } from 'framer-motion';
+import { useState } from 'react';
+import { EyeIcon, ArrowDownTrayIcon, HeartIcon } from '@heroicons/react/24/outline';
+import { HeartIcon as HeartIconSolid } from '@heroicons/react/24/solid';
+import SkinViewer from './SkinViewer';
+import type { Texture } from '@/lib/api';
+
+interface SkinCardProps {
+ texture: Texture;
+ isFavorited?: boolean;
+ onViewDetails: (texture: Texture) => void;
+ onToggleFavorite?: (textureId: number) => void;
+ onDownload?: (texture: Texture) => void;
+ showVisibilityBadge?: boolean;
+ showActions?: boolean;
+ customActions?: React.ReactNode;
+ index?: number;
+}
+
+export default function SkinCard({
+ texture,
+ isFavorited = false,
+ onViewDetails,
+ onToggleFavorite,
+ onDownload,
+ showVisibilityBadge = true,
+ showActions = true,
+ customActions,
+ index = 0
+}: SkinCardProps) {
+ const [isHovered, setIsHovered] = useState(false);
+ const [imageLoaded, setImageLoaded] = useState(false);
+ const [isDownloading, setIsDownloading] = useState(false);
+ const [isFavoriting, setIsFavoriting] = useState(false);
+
+ const handleDownload = async () => {
+ if (isDownloading) return;
+
+ setIsDownloading(true);
+
+ // 模拟下载延迟
+ setTimeout(() => {
+ if (onDownload) {
+ onDownload(texture);
+ } else {
+ window.open(texture.url, '_blank');
+ }
+ setIsDownloading(false);
+ }, 500);
+ };
+
+ const handleToggleFavorite = async () => {
+ if (isFavoriting || !onToggleFavorite) return;
+
+ setIsFavoriting(true);
+
+ // 模拟收藏操作延迟
+ setTimeout(() => {
+ onToggleFavorite(texture.id);
+ setIsFavoriting(false);
+ }, 300);
+ };
+
+ const getCardVariants = () => ({
+ hidden: {
+ opacity: 0,
+ y: 50,
+ scale: 0.9,
+ rotateX: -15
+ },
+ visible: {
+ opacity: 1,
+ y: 0,
+ scale: 1,
+ rotateX: 0,
+ transition: {
+ duration: 0.6,
+ delay: index * 0.1,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ type: "spring",
+ stiffness: 100,
+ damping: 15
+ }
+ },
+ hover: {
+ scale: 1.03,
+ y: -8,
+ rotateX: 5,
+ transition: {
+ duration: 0.3,
+ ease: "easeOut"
+ }
+ }
+ });
+
+ const getActionButtonVariants = () => ({
+ initial: { scale: 0, opacity: 0 },
+ hover: {
+ scale: 1,
+ opacity: 1,
+ transition: {
+ duration: 0.2,
+ delay: 0.1,
+ type: "spring",
+ stiffness: 300,
+ damping: 20
+ }
+ },
+ tap: {
+ scale: 0.9,
+ transition: { duration: 0.1 }
+ }
+ });
+
+ const getTagVariants = () => ({
+ initial: { scale: 0.8, opacity: 0 },
+ animate: {
+ scale: 1,
+ opacity: 1,
+ transition: {
+ duration: 0.3,
+ delay: 0.2 + index * 0.05,
+ type: "spring",
+ stiffness: 200,
+ damping: 15
+ }
+ },
+ hover: {
+ scale: 1.05,
+ transition: { duration: 0.2 }
+ }
+ });
+
+ return (
+ setIsHovered(true)}
+ onHoverEnd={() => setIsHovered(false)}
+ className="group relative bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden border border-white/20 dark:border-gray-700/30"
+ style={{
+ perspective: '1000px',
+ transformStyle: 'preserve-3d'
+ }}
+ >
+ {/* 3D预览区域 */}
+
+ {/* 加载状态 */}
+
+ {!imageLoaded && (
+
+
+
+ )}
+
+
+ {texture.type === 'SKIN' ? (
+
setImageLoaded(true)}
+ />
+ ) : (
+
+ )}
+
+ {/* 悬停操作按钮 */}
+ {showActions && (
+
+
+
onViewDetails(texture)}
+ className="bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white p-3 rounded-full shadow-lg transition-all duration-200 backdrop-blur-sm"
+ title="详细预览"
+ >
+
+
+
+
+
+ {onDownload !== false && (
+
+ {isDownloading ? (
+
+
+
+ ) : (
+
+ )}
+
+ )}
+
+ {onToggleFavorite && (
+
+ {isFavoriting ? (
+
+
+
+ ) : isFavorited ? (
+
+
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ )}
+
+ {/* 标签 */}
+
+
+ {texture.type === 'SKIN' ? '皮肤' : '披风'}
+
+ {texture.is_slim && (
+
+ 细臂
+
+ )}
+ {showVisibilityBadge && !texture.is_public && (
+
+ 私密
+
+ )}
+
+
+ {/* 悬停时的光效 */}
+
+ {isHovered && (
+
+ )}
+
+
+
+ {/* Texture Info */}
+
+
+ {texture.name}
+
+ {texture.description && (
+
+ {texture.description}
+
+ )}
+
+ {/* Stats */}
+
+
+ {onToggleFavorite && (
+
+
+
+
+ {texture.favorite_count || 0}
+
+ )}
+
+
+ {texture.download_count || 0}
+
+
+
+ {texture.uploader && (
+
+ by {texture.uploader.username}
+
+ )}
+
+
+
+ {/* Custom Actions */}
+ {customActions && (
+
+ {customActions}
+
+ )}
+
+
+ {/* 底部装饰条 */}
+
+
+ );
+}
+
diff --git a/src/components/SkinDetailModal.tsx b/src/components/SkinDetailModal.tsx
index f2bfe8e..d56bd0b 100644
--- a/src/components/SkinDetailModal.tsx
+++ b/src/components/SkinDetailModal.tsx
@@ -1,8 +1,8 @@
'use client';
import { useState, useEffect } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { XMarkIcon, PlayIcon, PauseIcon, ArrowPathIcon, ForwardIcon} from '@heroicons/react/24/outline';
+import { motion, AnimatePresence, useSpring, useTransform } from 'framer-motion';
+import { XMarkIcon, PlayIcon, PauseIcon, ArrowPathIcon, ForwardIcon } from '@heroicons/react/24/outline';
import SkinViewer from './SkinViewer';
interface SkinDetailModalProps {
@@ -22,13 +22,21 @@ interface SkinDetailModalProps {
username: string;
};
} | null;
+ isExternalPreview?: boolean;
}
-export default function SkinDetailModal({ isOpen, onClose, texture }: SkinDetailModalProps) {
+export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPreview = false }: SkinDetailModalProps) {
const [isPlaying, setIsPlaying] = useState(false);
- const [currentAnimation, setCurrentAnimation] = useState<'idle' | 'walking' | 'running' | 'jumping'>('idle');
- const [autoRotate, setAutoRotate] = useState(true);
+ const [currentAnimation, setCurrentAnimation] = useState<'idle' | 'walking' | 'running' | 'swimming'>('idle');
+ const [autoRotate, setAutoRotate] = useState(!isExternalPreview);
const [rotation, setRotation] = useState(true);
+ const [isMinimized, setIsMinimized] = useState(false);
+ const [activeTab, setActiveTab] = useState<'preview' | 'info' | 'settings'>('preview');
+
+ // 弹簧动画配置
+ const springConfig = { stiffness: 300, damping: 30 };
+ const scale = useSpring(1, springConfig);
+ const rotate = useSpring(0, springConfig);
// 重置状态当对话框关闭时
useEffect(() => {
@@ -37,6 +45,8 @@ export default function SkinDetailModal({ isOpen, onClose, texture }: SkinDetail
setCurrentAnimation('idle');
setAutoRotate(true);
setRotation(true);
+ setIsMinimized(false);
+ setActiveTab('preview');
}
}, [isOpen]);
@@ -63,14 +73,101 @@ export default function SkinDetailModal({ isOpen, onClose, texture }: SkinDetail
setCurrentAnimation('running');
break;
case '4':
- setCurrentAnimation('jumping');
+ setCurrentAnimation('swimming');
+ break;
+ case 'm':
+ case 'M':
+ setIsMinimized(!isMinimized);
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
- }, [isOpen, onClose, isPlaying]);
+ }, [isOpen, onClose, isPlaying, isMinimized]);
+
+ // 动画控制函数
+ const handleAnimationChange = (animation: 'idle' | 'walking' | 'running' | 'swimming') => {
+ setCurrentAnimation(animation);
+ // 添加触觉反馈
+ if (navigator.vibrate) {
+ navigator.vibrate(50);
+ }
+ };
+
+ const getModalVariants = () => ({
+ initial: {
+ opacity: 0,
+ scale: 0.7,
+ rotateX: -15,
+ y: 50
+ },
+ animate: {
+ opacity: 1,
+ scale: 1,
+ rotateX: 0,
+ y: 0,
+ transition: {
+ duration: 0.5,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ type: "spring",
+ stiffness: 100,
+ damping: 15
+ }
+ },
+ exit: {
+ opacity: 0,
+ scale: 0.7,
+ rotateX: 15,
+ y: 50,
+ transition: {
+ duration: 0.3,
+ ease: "easeIn"
+ }
+ },
+ minimized: {
+ scale: 0.8,
+ y: window.innerHeight - 200,
+ x: window.innerWidth - 300,
+ width: 280,
+ height: 150,
+ transition: {
+ duration: 0.4,
+ ease: "easeInOut"
+ }
+ }
+ });
+
+ const getAnimationButtonVariants = (isActive: boolean) => ({
+ initial: { scale: 0.9, opacity: 0.8 },
+ animate: {
+ scale: 1,
+ opacity: 1,
+ transition: {
+ duration: 0.2,
+ type: "spring",
+ stiffness: 300,
+ damping: 20
+ }
+ },
+ hover: {
+ scale: 1.05,
+ transition: { duration: 0.2 }
+ },
+ tap: {
+ scale: 0.95,
+ transition: { duration: 0.1 }
+ },
+ active: {
+ scale: 1.02,
+ transition: {
+ duration: 0.2,
+ type: "spring",
+ stiffness: 400,
+ damping: 25
+ }
+ }
+ });
if (!texture) return null;
@@ -81,235 +178,306 @@ export default function SkinDetailModal({ isOpen, onClose, texture }: SkinDetail
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
- className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
+ className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={onClose}
+ style={{
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ isolation: 'isolate'
+ }}
>
e.stopPropagation()}
>
{/* Header */}
-
+
-
+
{texture.name}
-
+
-
+
{texture.type === 'SKIN' ? '皮肤' : '披风'}
-
+
{texture.is_slim && (
-
+
细臂
-
+
)}
-
+ setIsMinimized(!isMinimized)}
+ className="p-2 text-gray-500 hover:text-orange-500 dark:text-gray-400 dark:hover:text-orange-400 rounded-full hover:bg-orange-100 dark:hover:bg-orange-900/20 transition-all duration-200"
+ whileHover={{ scale: 1.1 }}
+ whileTap={{ scale: 0.9 }}
+ initial={{ opacity: 0, rotate: -180 }}
+ animate={{ opacity: 1, rotate: 0 }}
+ transition={{ delay: 0.6 }}
+ title={isMinimized ? "最大化" : "最小化"}
+ >
+
+
+
+
+
+
+
+
+
+ {!isMinimized && (
+
+ {/* 3D 预览区域 */}
+
-
-
-
-
-
-
- {/* 3D 预览区域 */}
-
-
- {/* 控制面板 */}
-
- {/* 动画控制 */}
-
-
-
- 动画控制
-
-
-
-
setCurrentAnimation('idle')}
- className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${
- currentAnimation === 'idle'
- ? 'bg-orange-500 text-white shadow-lg'
- : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
- }`}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
+
+
- 静止
-
-
- setCurrentAnimation('walking')}
- className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${
- currentAnimation === 'walking'
- ? 'bg-orange-500 text-white shadow-lg'
- : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
- }`}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
- 步行
-
-
- setCurrentAnimation('running')}
- className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${
- currentAnimation === 'running'
- ? 'bg-orange-500 text-white shadow-lg'
- : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
- }`}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
- 跑步
-
-
- setCurrentAnimation('jumping')}
- className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${
- currentAnimation === 'jumping'
- ? 'bg-orange-500 text-white shadow-lg'
- : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
- }`}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
- 跳跃
-
+
+
-
+
- {/* 视角控制 */}
-
-
-
- 视角控制
-
-
-
-
setAutoRotate(!autoRotate)}
- className={`w-full p-3 rounded-lg text-sm font-medium transition-all duration-200 flex items-center justify-center ${
- autoRotate
- ? 'bg-blue-500 text-white shadow-lg'
- : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
- }`}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
+ {/* 控制面板 */}
+
+ {/* 动画控制 */}
+
+
-
- {autoRotate ? '停止旋转' : '自动旋转'}
-
+
+ 动画控制
+
-
setRotation(!rotation)}
- className={`w-full p-3 rounded-lg text-sm font-medium transition-all duration-200 flex items-center justify-center ${
- rotation
- ? 'bg-green-500 text-white shadow-lg'
- : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
- }`}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
-
- {rotation ? '禁用控制' : '启用控制'}
-
-
-
-
- {/* 皮肤信息 */}
-
-
- 📋
- 皮肤信息
-
-
-
- {texture.description && (
-
-
描述
-
{texture.description}
-
- )}
-
-
-
-
收藏数
-
{texture.favorite_count || 0}
-
-
-
下载数
-
{texture.download_count || 0}
-
+
+ {[
+ { key: 'idle', label: '静止', icon: null },
+ { key: 'walking', label: '步行', icon: null },
+ { key: 'running', label: '跑步', icon: null },
+ { key: 'swimming', label: '游泳', icon: null }
+ ].map((anim, i) => (
+ handleAnimationChange(anim.key as any)}
+ className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${
+ currentAnimation === anim.key
+ ? 'bg-gradient-to-r from-orange-500 to-amber-500 text-white shadow-lg transform scale-105'
+ : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-orange-100 dark:hover:bg-orange-900/20 border border-orange-200 dark:border-gray-600 hover:border-orange-300 dark:hover:border-orange-500'
+ }`}
+ initial={{ opacity: 0, y: 20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.6 + i * 0.1 }}
+ >
+ {anim.label}
+
+ ))}
-
- {texture.uploader && (
-
-
上传者
-
{texture.uploader.username}
-
- )}
-
- {texture.created_at && (
-
-
上传时间
-
- {new Date(texture.created_at).toLocaleDateString('zh-CN')}
-
-
- )}
-
-
+
- {/* 快捷键提示 */}
-
-
- ⌨️
- 快捷键
-
-
-
空格 播放/暂停
-
1 静止
-
2 步行
-
3 跑步
-
4 跳跃
-
ESC 关闭
-
-
+ {/* 信息面板 */}
+
+
+ 皮肤信息
+
+
+ {texture.description && (
+
+
+ {texture.description}
+
+
+ )}
+
+
+ {texture.uploader && (
+
+ 上传者:
+ {texture.uploader.username}
+
+ )}
+
+ {texture.created_at && (
+
+ 上传时间:
+
+ {new Date(texture.created_at).toLocaleDateString()}
+
+
+ )}
+
+
+ 收藏数:
+ {texture.favorite_count || 0}
+
+
+
+ 下载数:
+ {texture.download_count || 0}
+
+
+
+
+ {/* 快捷键提示 */}
+
+ 快捷键:
+ 空格 - 播放/暂停 | 1-4 - 切换动画 | M - 最小化 | ESC - 关闭
+
+
-
+ )}
+
+ {/* 最小化时的内容 */}
+ {isMinimized && (
+
+
+
+
+
+
+ {texture.name}
+
+
+
+ )}
)}
);
-}
+}
\ No newline at end of file
diff --git a/src/components/SkinViewer.tsx b/src/components/SkinViewer.tsx
index cecd109..a4b76d9 100644
--- a/src/components/SkinViewer.tsx
+++ b/src/components/SkinViewer.tsx
@@ -15,6 +15,7 @@ interface SkinViewerProps {
running?: boolean; // 新增:跑步动画
jumping?: boolean; // 新增:跳跃动画
rotation?: boolean; // 新增:旋转控制
+ isExternalPreview?: boolean; // 新增:是否为外部预览
}
export default function SkinViewer({
@@ -29,6 +30,7 @@ export default function SkinViewer({
running = false,
jumping = false,
rotation = true,
+ isExternalPreview = false, // 新增:默认为false
}: SkinViewerProps) {
const canvasRef = useRef
(null);
const viewerRef = useRef(null);
@@ -92,11 +94,19 @@ export default function SkinViewer({
// 设置背景和控制选项 - 参考blessingskin
viewer.background = null; // 透明背景
- viewer.autoRotate = autoRotate && !walking && !running && !jumping; // 只在无动画时自动旋转
+ viewer.autoRotate = false; // 完全禁用自动旋转
+
+ // 外部预览时禁用所有动画和旋转
+ if (isExternalPreview) {
+ viewer.autoRotate = false;
+ viewer.controls.enableRotate = false; // 禁用旋转控制
+ viewer.controls.enableZoom = false; // 禁用缩放
+ } else {
+ viewer.autoRotate = autoRotate && !walking && !running && !jumping;
+ viewer.controls.enableRotate = rotation; // 根据参数控制旋转
+ viewer.controls.enableZoom = true; // 启用缩放
+ }
- // 设置交互控制
- viewer.controls.enableRotate = rotation; // 根据参数控制旋转
- viewer.controls.enableZoom = true; // 启用缩放
viewer.controls.enablePan = false; // 禁用平移
console.log('3D皮肤查看器初始化成功');
@@ -118,7 +128,7 @@ export default function SkinViewer({
}
}
};
- }, [skinUrl, capeUrl, isSlim, width, height, autoRotate, walking, running, jumping, rotation, imageLoaded, hasError]);
+ }, [skinUrl, capeUrl, isSlim, width, height, autoRotate, walking, running, jumping, rotation, imageLoaded, hasError, isExternalPreview]);
// 控制动画效果 - 参考 Blessing Skin 的实现
useEffect(() => {
@@ -126,29 +136,36 @@ export default function SkinViewer({
const viewer = viewerRef.current;
- // 根据优先级设置动画 - 参考 Blessing Skin 的 animationFactories
- if (running) {
- // 跑步动画
- viewer.animation = new RunningAnimation();
- console.log('启用跑步动画');
- } else if (walking) {
- // 普通步行动画
- viewer.animation = new WalkingAnimation();
- console.log('启用步行动画');
- } else if (jumping) {
- // 飞行动画作为跳跃
- viewer.animation = new FlyingAnimation();
- console.log('启用跳跃动画');
- } else {
- // 静止动画
+ // 外部预览时只使用静止动画,禁用所有其他动画
+ if (isExternalPreview) {
viewer.animation = new IdleAnimation();
- console.log('启用静止动画');
+ viewer.autoRotate = false;
+ console.log('外部预览模式:启用静止动画,禁用旋转');
+ } else {
+ // 根据优先级设置动画 - 参考 Blessing Skin 的 animationFactories
+ if (running) {
+ // 跑步动画
+ viewer.animation = new RunningAnimation();
+ console.log('启用跑步动画');
+ } else if (walking) {
+ // 普通步行动画
+ viewer.animation = new WalkingAnimation();
+ console.log('启用步行动画');
+ } else if (jumping) {
+ // 飞行动画作为跳跃
+ viewer.animation = new FlyingAnimation();
+ console.log('启用跳跃动画');
+ } else {
+ // 静止动画
+ viewer.animation = new IdleAnimation();
+ console.log('启用静止动画');
+ }
+
+ // 更新自动旋转状态
+ viewer.autoRotate = autoRotate && !walking && !running && !jumping;
}
- // 更新自动旋转状态
- viewer.autoRotate = autoRotate && !walking && !running && !jumping;
-
- }, [walking, running, jumping, autoRotate]);
+ }, [walking, running, jumping, autoRotate, isExternalPreview]);
// 当皮肤URL改变时更新
useEffect(() => {
diff --git a/src/components/profile/CharacterCard.tsx b/src/components/profile/CharacterCard.tsx
new file mode 100644
index 0000000..0545afe
--- /dev/null
+++ b/src/components/profile/CharacterCard.tsx
@@ -0,0 +1,154 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import { UserIcon, PencilIcon, TrashIcon, CheckIcon } from '@heroicons/react/24/outline';
+import SkinViewer from '@/components/SkinViewer';
+import type { Profile } from '@/lib/api';
+
+interface CharacterCardProps {
+ profile: Profile;
+ skinUrl?: string;
+ isSlim?: boolean;
+ isEditing?: boolean;
+ editName?: string;
+ onEdit: (uuid: string, currentName: string) => void;
+ onSave: (uuid: string) => void;
+ onCancel: () => void;
+ onDelete: (uuid: string) => void;
+ onSetActive: (uuid: string) => void;
+ onSelectSkin: (uuid: string) => void;
+ onEditNameChange: (name: string) => void;
+}
+
+export default function CharacterCard({
+ profile,
+ skinUrl,
+ isSlim,
+ isEditing,
+ editName,
+ onEdit,
+ onSave,
+ onCancel,
+ onDelete,
+ onSetActive,
+ onSelectSkin,
+ onEditNameChange
+}: CharacterCardProps) {
+ return (
+
+
+ {isEditing ? (
+ onEditNameChange(e.target.value)}
+ className="text-lg font-semibold bg-transparent border-b border-orange-500 focus:outline-none text-gray-900 dark:text-white flex-1 mr-2"
+ onBlur={() => onSave(profile.uuid)}
+ onKeyPress={(e) => e.key === 'Enter' && onSave(profile.uuid)}
+ autoFocus
+ />
+ ) : (
+
{profile.name}
+ )}
+ {profile.is_active && (
+
+
+ 当前使用
+
+ )}
+
+
+
+ {skinUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+ {/* 皮肤选择按钮 */}
+
onSelectSkin(profile.uuid)}
+ 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 }}
+ title="选择皮肤"
+ >
+
+
+
+
+ {/* 操作按钮 */}
+
+ {!profile.is_active && (
+
onSetActive(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"
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+ 使用
+
+ )}
+
+ {isEditing ? (
+ <>
+
onSave(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"
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+ 保存
+
+
+ 取消
+
+ >
+ ) : (
+ <>
+
onEdit(profile.uuid, 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"
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+
+ 编辑
+
+
onDelete(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"
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/src/components/profile/FavoritesTab.tsx b/src/components/profile/FavoritesTab.tsx
new file mode 100644
index 0000000..77129e6
--- /dev/null
+++ b/src/components/profile/FavoritesTab.tsx
@@ -0,0 +1,76 @@
+'use client';
+
+import { useState } from 'react';
+import { motion } from 'framer-motion';
+import { PhotoIcon, ArrowDownTrayIcon, EyeIcon, HeartIcon } from '@heroicons/react/24/outline';
+import { HeartIcon as HeartIconSolid } from '@heroicons/react/24/solid';
+import SkinCard from '@/components/SkinCard';
+import SkinDetailModal from '@/components/SkinDetailModal';
+import type { Texture } from '@/lib/api';
+
+interface FavoritesTabProps {
+ skins: Texture[];
+ onToggleFavorite: (skinId: number) => void;
+}
+
+export default function FavoritesTab({ skins, onToggleFavorite }: FavoritesTabProps) {
+ const [selectedSkin, setSelectedSkin] = useState(null);
+ const [showSkinDetail, setShowSkinDetail] = useState(false);
+
+ const handleViewDetails = (skin: Texture) => {
+ setSelectedSkin(skin);
+ setShowSkinDetail(true);
+ };
+
+ return (
+
+
+
收藏夹
+
+
+ {skins.length === 0 ? (
+
+
+
暂无收藏
+
去发现一些喜欢的皮肤吧!
+
+ ) : (
+
+ {skins.map((skin) => (
+ {
+ e.stopPropagation();
+ onToggleFavorite(skin.id);
+ }}
+ className="flex-1 px-3 py-2 border border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-lg transition-all duration-200"
+ >
+ 取消收藏
+
+ }
+ />
+ ))}
+
+ )}
+
+ {/* 皮肤详情模态框 */}
+ setShowSkinDetail(false)}
+ texture={selectedSkin}
+ />
+
+ );
+}
diff --git a/src/components/profile/MySkinsTab.tsx b/src/components/profile/MySkinsTab.tsx
new file mode 100644
index 0000000..7027d90
--- /dev/null
+++ b/src/components/profile/MySkinsTab.tsx
@@ -0,0 +1,107 @@
+'use client';
+
+import { useState } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import {
+ PhotoIcon,
+ ArrowDownTrayIcon,
+ EyeIcon,
+ TrashIcon,
+ CloudArrowUpIcon
+} from '@heroicons/react/24/outline';
+import SkinCard from '@/components/SkinCard';
+import SkinDetailModal from '@/components/SkinDetailModal';
+import type { Texture } from '@/lib/api';
+
+interface MySkinsTabProps {
+ skins: Texture[];
+ onUploadClick: () => void;
+ onToggleVisibility: (skinId: number) => void;
+ onDelete: (skinId: number) => void;
+}
+
+export default function MySkinsTab({
+ skins,
+ onUploadClick,
+ onToggleVisibility,
+ onDelete
+}: MySkinsTabProps) {
+ const [selectedSkin, setSelectedSkin] = useState(null);
+ const [showSkinDetail, setShowSkinDetail] = useState(false);
+
+ const handleViewDetails = (skin: Texture) => {
+ setSelectedSkin(skin);
+ setShowSkinDetail(true);
+ };
+
+ return (
+
+
+
我的皮肤
+
+
+ 上传皮肤
+
+
+
+ {skins.length === 0 ? (
+
+
+
暂无皮肤
+
上传你的第一个Minecraft皮肤吧!
+
+ ) : (
+
+ {skins.map((skin) => (
+
+
+
+
+ }
+ />
+ ))}
+
+ )}
+
+ {/* 皮肤详情模态框 */}
+
setShowSkinDetail(false)}
+ texture={selectedSkin}
+ />
+
+ );
+}
diff --git a/src/components/profile/ProfileSidebar.tsx b/src/components/profile/ProfileSidebar.tsx
new file mode 100644
index 0000000..90f16d4
--- /dev/null
+++ b/src/components/profile/ProfileSidebar.tsx
@@ -0,0 +1,79 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import {
+ UserIcon,
+ PhotoIcon,
+ HeartIcon,
+ Cog6ToothIcon,
+ ArrowLeftOnRectangleIcon
+} from '@heroicons/react/24/outline';
+
+interface ProfileSidebarProps {
+ activeTab: string;
+ onTabChange: (tab: string) => void;
+ skinCount: number;
+ favoriteCount: number;
+ profilesCount: number;
+ onLogout: () => void;
+}
+
+export default function ProfileSidebar({
+ activeTab,
+ onTabChange,
+ skinCount,
+ favoriteCount,
+ profilesCount,
+ onLogout
+}: ProfileSidebarProps) {
+ const menuItems = [
+ { id: 'characters', name: '角色管理', icon: UserIcon, count: profilesCount },
+ { id: 'skins', name: '我的皮肤', icon: PhotoIcon, count: skinCount },
+ { id: 'favorites', name: '收藏夹', icon: HeartIcon, count: favoriteCount },
+ { id: 'settings', name: '账户设置', icon: Cog6ToothIcon },
+ ];
+
+ return (
+
+
+
+
+
+ 退出登录
+
+
+ );
+}
diff --git a/src/components/profile/UploadSkinModal.tsx b/src/components/profile/UploadSkinModal.tsx
new file mode 100644
index 0000000..d72fd1b
--- /dev/null
+++ b/src/components/profile/UploadSkinModal.tsx
@@ -0,0 +1,199 @@
+'use client';
+
+import { useState } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { XMarkIcon, CloudArrowUpIcon } from '@heroicons/react/24/outline';
+import { messageManager } from '@/components/MessageNotification';
+
+interface UploadSkinModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onUpload: (file: File, data: {
+ name: string;
+ description: string;
+ type: 'SKIN' | 'CAPE';
+ is_public: boolean;
+ is_slim: boolean;
+ }) => Promise;
+ isUploading?: boolean;
+ uploadProgress?: number;
+}
+
+export default function UploadSkinModal({
+ isOpen,
+ onClose,
+ onUpload,
+ isUploading = false,
+ uploadProgress = 0
+}: UploadSkinModalProps) {
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [skinData, setSkinData] = useState({
+ name: '',
+ description: '',
+ type: 'SKIN' as 'SKIN' | 'CAPE',
+ is_public: false,
+ is_slim: false
+ });
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ setSelectedFile(file);
+ }
+ };
+
+ const handleSubmit = async () => {
+ if (!selectedFile || !skinData.name.trim()) {
+ messageManager.warning('请选择皮肤文件并输入皮肤名称', { duration: 3000 });
+ return;
+ }
+
+ await onUpload(selectedFile, skinData);
+ // 重置表单
+ setSelectedFile(null);
+ setSkinData({
+ name: '',
+ description: '',
+ type: 'SKIN',
+ is_public: false,
+ is_slim: false
+ });
+ };
+
+ return (
+
+ {isOpen && (
+
+
+
+
上传皮肤
+
+
+
+
+
+
+
+
+ 点击选择文件或拖拽到此处
+
+
+
+ {selectedFile && (
+
+ 已选择: {selectedFile.name}
+
+ )}
+
+
+
+
+
+ setSkinData(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="请输入皮肤名称"
+ />
+
+
+
+
+
+
+
+
+ {isUploading && (
+
+ )}
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/profile/UserProfileCard.tsx b/src/components/profile/UserProfileCard.tsx
new file mode 100644
index 0000000..34aff82
--- /dev/null
+++ b/src/components/profile/UserProfileCard.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import { UserCircleIcon } from '@heroicons/react/24/outline';
+
+interface UserProfileCardProps {
+ username?: string;
+ email?: string;
+ avatar?: string;
+ skinCount: number;
+ favoriteCount: number;
+ points: number;
+}
+
+export default function UserProfileCard({
+ username,
+ email,
+ avatar,
+ skinCount,
+ favoriteCount,
+ points
+}: UserProfileCardProps) {
+ return (
+
+ {/* 装饰性背景元素 */}
+
+
+
+
+
+
+ {avatar ? (
+
+ ) : (
+
+
+
+ )}
+
+
{username || '用户名'}
+
{email || '邮箱地址'}
+
+
+
+
+
+ {skinCount}
+ 皮肤
+
+
+ {favoriteCount}
+ 收藏
+
+
+ {points}
+ 积分
+
+
+
+
+ );
+}