Initial commit: CarrotSkin project setup

This commit is contained in:
2025-12-05 20:07:50 +08:00
parent a9ff72a9bf
commit f5e4c2a04b
24 changed files with 5389 additions and 2145 deletions

View File

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

View File

@@ -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({
<body className={inter.className}>
<AuthProvider>
<Navbar />
<MainContent>{children}</MainContent>
<PageTransition>
<MainContent>{children}</MainContent>
</PageTransition>
<ErrorNotificationContainer />
<MessageNotificationContainer />
<ScrollToTop />
</AuthProvider>
</body>

View File

@@ -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 (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-orange-50 via-white to-amber-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<motion.div
className="text-center px-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
>
{/* 404 数字 */}
<motion.div
className="mb-8"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.6, type: 'spring', stiffness: 200 }}
>
<h1 className="text-9xl font-black bg-gradient-to-r from-orange-400 via-orange-500 to-amber-500 bg-clip-text text-transparent mb-4">
404
</h1>
<div className="w-24 h-1 bg-gradient-to-r from-orange-400 to-amber-500 mx-auto rounded-full" />
</motion.div>
{/* 错误信息 */}
<motion.div
className="mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.6 }}
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-md mx-auto leading-relaxed">
访
</p>
</motion.div>
{/* Minecraft 风格的装饰 */}
<motion.div
className="mb-8 flex justify-center"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.6, duration: 0.6 }}
>
<div className="relative">
<div className="w-16 h-16 bg-gradient-to-br from-orange-400 to-amber-500 rounded-lg flex items-center justify-center transform rotate-12 shadow-lg">
<span className="text-2xl font-bold text-white">?</span>
</div>
<div className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center animate-pulse">
<span className="text-white text-xs font-bold">!</span>
</div>
</div>
</motion.div>
{/* 操作按钮 */}
<motion.div
className="flex flex-col sm:flex-row gap-4 justify-center items-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8, duration: 0.6 }}
>
<Link
href="/"
className="inline-flex items-center px-6 py-3 bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white font-semibold rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105"
>
<HomeIcon className="w-5 h-5 mr-2" />
</Link>
<button
onClick={() => window.history.back()}
className="inline-flex items-center px-6 py-3 border-2 border-orange-500 text-orange-500 hover:bg-orange-500 hover:text-white font-semibold rounded-xl transition-all duration-200"
>
<ArrowLeftIcon className="w-5 h-5 mr-2" />
</button>
</motion.div>
{/* 额外的帮助信息 */}
<motion.div
className="mt-8 text-sm text-gray-500 dark:text-gray-400"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1, duration: 0.6 }}
>
<p>
<Link href="/contact" className="text-orange-500 hover:text-orange-600 underline">
</Link>
</p>
</motion.div>
</motion.div>
{/* 背景装饰 */}
<div className="fixed inset-0 -z-10 overflow-hidden">
<div className="absolute top-1/4 left-1/4 w-64 h-64 bg-orange-200/20 dark:bg-orange-900/20 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-amber-200/20 dark:bg-amber-900/20 rounded-full blur-3xl" />
</div>
</div>
);
return <NotFoundPage />;
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<Texture[]>([]);
@@ -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 (
<motion.div
<SkinCard
key={texture.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="group relative bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 overflow-hidden border border-white/20 dark:border-gray-700/30"
>
{/* 3D预览区域 - 更紧凑 */}
<div className="relative aspect-square bg-gradient-to-br from-orange-50 to-amber-50 dark:from-gray-700 dark:to-gray-600 overflow-hidden">
<SkinViewer
skinUrl={texture.url}
isSlim={texture.is_slim}
width={300}
height={300}
className="w-full h-full"
autoRotate={true}
walking={false}
/>
{/* 悬停操作按钮 */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
<div className="flex gap-3">
<motion.button
onClick={() => 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="详细预览"
>
<EyeIcon className="w-5 h-5" />
</motion.button>
<motion.button
onClick={() => 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="查看原图"
>
<ArrowDownTrayIcon className="w-5 h-5" />
</motion.button>
</div>
</div>
{/* 标签 */}
<div className="absolute top-3 right-3 flex gap-1.5">
<motion.span
className={`px-2 py-1 text-white text-xs rounded-full font-medium backdrop-blur-sm ${
texture.type === 'SKIN' ? 'bg-blue-500/80' : 'bg-purple-500/80'
}`}
whileHover={{ scale: 1.05 }}
>
{texture.type === 'SKIN' ? '皮肤' : '披风'}
</motion.span>
{texture.is_slim && (
<motion.span
className="px-2 py-1 bg-pink-500/80 text-white text-xs rounded-full font-medium backdrop-blur-sm"
whileHover={{ scale: 1.05 }}
>
</motion.span>
)}
</div>
</div>
{/* Texture Info */}
<div className="p-4">
<h3 className="font-semibold text-gray-900 dark:text-white mb-1 truncate">{texture.name}</h3>
{texture.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2 leading-relaxed">
{texture.description}
</p>
)}
{/* Stats */}
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400 mb-4">
<div className="flex items-center space-x-3">
<motion.span
className="flex items-center space-x-1"
whileHover={{ scale: 1.05 }}
>
<HeartIcon className="w-4 h-4 text-red-400" />
<span className="font-medium">{texture.favorite_count}</span>
</motion.span>
<motion.span
className="flex items-center space-x-1"
whileHover={{ scale: 1.05 }}
>
<ArrowDownTrayIcon className="w-4 h-4 text-blue-400" />
<span className="font-medium">{texture.download_count}</span>
</motion.span>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2">
<motion.button
onClick={() => 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 }}
>
<EyeIcon className="w-4 h-4 mr-1" />
</motion.button>
<motion.button
onClick={() => 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 ? (
<HeartIconSolid className="w-4 h-4" />
) : (
<HeartIcon className="w-4 h-4" />
)}
</motion.button>
</div>
</div>
</motion.div>
texture={texture}
isFavorited={isFavorited}
onViewDetails={handleDetailView}
onToggleFavorite={isAuthenticated ? handleFavorite : undefined}
onDownload={(texture) => window.open(texture.url, '_blank')}
showVisibilityBadge={false}
/>
);
})}
</motion.div>