更新皮肤分类功能,添加帮助页面和Yggdrasil教程

This commit is contained in:
Mikuisnotavailable
2025-10-06 18:54:26 +08:00
parent 695001157a
commit 0645948fcc
46 changed files with 4180 additions and 1312 deletions

231
.gitignore vendored
View File

@@ -1,131 +1,134 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # Dependencies
/node_modules node_modules/
/.pnp .pnp
.pnp.* .pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing # Testing
/coverage coverage/
# next.js # Production build outputs
/.next/ .next/
/out/ out/
build/
dist/
# production # Environment variables
/build .env
.env.local
.env.development.local
.env.test.local
.env.production.local
# misc # Logs
.DS_Store
*.pem
# debug
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.pnpm-debug.log* pnpm-debug.log*
lerna-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# .gitignore
# 依赖目录
node_modules/
# Next.js 构建输出
.next/
out/
# 生产依赖
dist/
# 缓存文件
.cache/
*.cache
# 环境变量
.env*.local
.env
# 系统文件
.DS_Store
Thumbs.db
# 日志文件
*.log
logs/ logs/
*.log
# 编辑器/IDE 配置 # Editor directories and files
.idea/ .idea/
.vscode/ .vscode/
*.suo *.swp
*.ntvs* *.swo
*.njsproj
*.sln
*.sw?
# 测试相关
coverage/
.nyc_output/
# TypeScript 编译输出
*.js
*.js.map
!next.config.js
# Tailwind CSS
*.css.map
# macOS 系统文件
._*
# Windows 系统文件
[Dd]esktop.ini
# 包管理器锁文件(根据你使用的包管理器选择)
# yarn.lock # 如果使用 Yarn 请取消注释
# package-lock.json # 如果使用 npm 请取消注释
# pnpm-lock.yaml # 如果使用 pnpm 请取消注释
# Node.js 依赖
node_modules/
# Next.js 特定构建输出
.next/
out/
# 环境变量
.env*.local
.env
# macOS
.DS_Store
# Windows
Thumbs.db
Desktop.ini
# Linux
*~ *~
# VS Code
.vscode/
# IntelliJ IDEA # OS generated files
.idea/ .DS_Store
# 保留 package-lock.json Thumbs.db
!package-lock.json Thumbs.db:encryptable
nehahn/
ehahn.db
[Dd]esktop.ini
*.cab
*.msi
*.msix
*.msm
*.msp
*.lnk
# 忽略 npm 调试日志 # TypeScript
npm-debug.log* *.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Vercel
.vercel/
#保留重要文件
!package-lock.json

127
README.md
View File

@@ -1 +1,126 @@
#3D 预览还没实现 # Minecraft 皮肤管理平台
一个基于 Next.js 构建的现代化 Minecraft 皮肤管理平台,支持皮肤上传、管理、角色创建和 Yggdrasil 认证。
## 功能特性
- 📤 **皮肤上传与管理** - 支持上传、预览和管理多个 Minecraft 皮肤
- 👤 **角色中心** - 创建和管理游戏角色,支持多角色切换
- 🔐 **外置登录** - 通过拖拽功能实现与 PCL2 等启动器的 Yggdrasil 认证
- 📚 **使用教程** - 提供基础教程、Yggdrasil 教程等帮助文档
- 🎨 **响应式设计** - 适配桌面和移动设备的现代化界面
- 🌙 **暗色模式** - 支持亮色/暗色主题切换
## 技术栈
- **前端框架**: Next.js 14 + React 18
- **类型系统**: TypeScript
- **样式方案**: Tailwind CSS 4 + Radix UI
- **认证系统**: NextAuth.js
- **API 调用**: Axios
- **图标库**: Lucide React
## 安装与运行
### 前置要求
- Node.js 20+
- npm 或 yarn
### 安装步骤
1. 克隆项目
```bash
git clone [仓库地址]
cd my-app
```
2. 安装依赖
```bash
npm install
# 或
yarn install
```
3. 启动开发服务器
```bash
npm run dev
# 或
yarn dev
```
4. 构建生产版本
```bash
npm run build
npm start
# 或
yarn build
yarn start
```
## 项目结构
```
├── public/ # 静态资源文件
│ ├── skins/ # 皮肤文件存储
│ └── images/ # 其他图片资源
├── src/ # 源代码目录
│ ├── app/ # Next.js App Router
│ │ ├── (auth)/ # 认证相关页面
│ │ ├── api/ # API 路由
│ │ ├── character-center/ # 角色中心页面
│ │ ├── dashboard/ # 皮肤管理仪表盘
│ │ ├── help/ # 帮助文档页面
│ │ ├── skins/ # 皮肤上传和管理
│ │ └── user-home/ # 用户主页
│ ├── components/ # 可复用组件
│ │ ├── Navbar.tsx # 导航栏组件
│ │ ├── auth/ # 认证相关组件
│ │ ├── skins/ # 皮肤相关组件
│ │ └── ui/ # UI 组件库
│ ├── lib/ # 工具函数和 API 调用
│ ├── styles/ # 全局样式和主题
│ └── types/ # TypeScript 类型定义
└── next.config.js # Next.js 配置文件
```
## 使用指南
### 皮肤上传
1. 登录平台后,在用户主页点击"上传皮肤"卡片
2. 选择符合要求的 PNG 格式皮肤文件
3. 上传成功后可在"我的皮肤"中查看和管理
### Yggdrasil 认证
1. 在用户主页找到"外置登录"卡片
2. 拖拽此卡片到 PCL2 等支持 Yggdrasil 认证的启动器
3. 启动器将自动完成认证流程
### 角色管理
1. 点击"角色中心"卡片进入角色管理界面
2. 可以创建新角色、编辑现有角色信息
3. 为不同角色分配不同的皮肤
## 注意事项
- 皮肤文件必须为 PNG 格式,且符合 Minecraft 皮肤尺寸规范
- 3D 预览功能尚未实现,目前使用 2D 预览
- 部分功能可能需要后端 API 支持,请确保相关接口已正确配置
## 许可证
[MIT](LICENSE)
## 贡献指南
欢迎提交 Issue 和 Pull Request
## 联系方式
如有问题或建议,请通过以下方式联系我们:
- Email: [项目邮箱]
- GitHub: [项目仓库地址]

49
middleware.ts Normal file
View File

@@ -0,0 +1,49 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';
// 更直接的中间件实现
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 定义需要检查登录状态的页面
const isRootPage = pathname === '/';
const isLoginPage = pathname === '/login';
const isRegisterPage = pathname === '/register';
// 只对这三个页面进行检查
const shouldCheckAuth = isRootPage || isLoginPage || isRegisterPage;
if (!shouldCheckAuth) {
return NextResponse.next();
}
try {
// 使用cookie存储的next-auth会话信息来检查登录状态
// 从请求头中提取cookie信息
const authCookie = request.cookies.get('next-auth.session-token') ||
request.cookies.get('__Secure-next-auth.session-token');
// 如果存在auth cookie认为用户已登录重定向到用户主页
if (authCookie) {
const url = request.nextUrl.clone();
url.pathname = '/user-home';
return NextResponse.redirect(url);
}
// 不存在auth cookie允许访问原页面
return NextResponse.next();
} catch (error) {
console.error('登录状态检查错误:', error);
// 发生错误时,为了安全起见,默认允许访问原页面
return NextResponse.next();
}
}
// 配置中间件适用的路径
export const config = {
// 直接匹配三个目标页面
matcher: ['/', '/login', '/register'],
};
// 重要提示:对于(auth)路由组中的页面Next.js路由系统会自动将'/login'和'/register'映射到正确的物理路径

5
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

781
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,6 @@
"dependencies": { "dependencies": {
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@react-three/drei": "^9.122.0",
"@react-three/fiber": "^8.18.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -22,8 +20,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1"
"three": "^0.152.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@@ -31,7 +28,6 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19.1.9", "@types/react": "^19.1.9",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/three": "^0.178.1",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.4.4", "eslint-config-next": "15.4.4",

BIN
public/test-skin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/test-skin2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/test-skin3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

View File

@@ -1,24 +1,121 @@
'use client';
import AuthForm from '@/components/auth/AuthForm'; import AuthForm from '@/components/auth/AuthForm';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image';
import { useSearchParams } from 'next/navigation';
export default function LoginPage() { export default function LoginPage() {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl') || '/';
return ( return (
<div className="max-w-md mx-auto"> <div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800 relative overflow-hidden pb-8">
<Card> {/* 背景装饰元素 - 渐变模糊效果 */}
<CardHeader> <div className="absolute inset-0 overflow-hidden pointer-events-none">
<CardTitle className="text-center">littlelan账户</CardTitle> <div className="absolute -top-20 -right-20 w-80 h-80 bg-emerald-400/20 rounded-full blur-3xl"></div>
</CardHeader> <div className="absolute -bottom-20 -left-20 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl"></div>
<CardContent> <div className="absolute top-1/3 left-1/4 w-64 h-64 bg-emerald-300/10 rounded-full blur-3xl"></div>
<AuthForm type="login" /> </div>
<div className="mt-4 text-center text-sm">
? {/* 主要内容区域 */}
<Link href="/register" className="ml-1 text-blue-500 hover:underline"> <div className="relative z-10 flex flex-col items-center justify-center min-h-screen px-4 py-12">
<div className="w-full max-w-md">
</Link> {/* 装饰性头部 */}
<div className="text-center mb-8 sm:mb-10">
<div className="flex justify-center mb-6">
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 bg-emerald-600 rounded-2xl transform -rotate-6 shadow-lg transition-all duration-500 hover:rotate-0">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-white rounded-xl transform rotate-6 flex items-center justify-center transition-all duration-500 hover:rotate-0">
<Image
src="/images/mc-favicon.ico"
alt="Logo"
width={40}
height={40}
className="rounded-xl w-8 h-8"
/>
</div>
</div>
</div>
<h1 className="text-3xl sm:text-4xl font-bold text-gray-800 dark:text-white mb-3 tracking-tight">
<span className="text-emerald-600 dark:text-emerald-400"> HITWH</span>
</h1>
<p className="text-gray-600 dark:text-gray-400 text-sm sm:text-base leading-relaxed max-w-sm mx-auto">
Minecraft皮肤
</p>
</div> </div>
</CardContent>
</Card> {/* 登录卡片 */}
<div className="relative group">
{/* 卡片阴影装饰 */}
<div className="absolute inset-0 bg-emerald-500 rounded-2xl transform rotate-2 opacity-20 transition-all duration-500 group-hover:rotate-1 group-hover:scale-102"></div>
<div className="absolute inset-0 bg-teal-400 rounded-2xl transform -rotate-1 opacity-15 transition-all duration-500 group-hover:-rotate-0.5 group-hover:scale-101"></div>
<Card className="relative bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm border border-emerald-200 dark:border-emerald-900/50 shadow-xl rounded-2xl overflow-hidden">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardHeader className="text-center pb-6 pt-8">
<CardTitle className="text-xl sm:text-2xl font-bold text-gray-800 dark:text-white">
🚪
</CardTitle>
<div className="w-16 h-1 bg-gradient-to-r from-emerald-500 to-teal-500 mx-auto mt-2 rounded-full"></div>
</CardHeader>
<CardContent className="space-y-6 px-6 pb-8">
{/* 使用统一的认证表单组件 */}
<AuthForm type="login" />
{/* 分割线 */}
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200 dark:border-gray-700"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-3 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded-full"></span>
</div>
</div>
{/* 注册链接 */}
<div className="text-center space-y-4">
<p className="text-gray-600 dark:text-gray-400">
</p>
<Button asChild variant="outline" className="w-full border-emerald-600 text-emerald-600 hover:bg-emerald-50 dark:border-emerald-400 dark:text-emerald-400 dark:hover:bg-emerald-900/20 rounded-xl py-6 transition-all duration-300 transform hover:-translate-y-1">
<Link href={`/register?callbackUrl=${encodeURIComponent(callbackUrl)}`}>
🎮
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
{/* 底部装饰 */}
<div className="text-center mt-8 sm:mt-10">
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2 text-xs sm:text-sm text-gray-500 dark:text-gray-400">
<span>🎨 </span>
<span></span>
<span>🌍 </span>
<span></span>
<span> </span>
<span></span>
<span>👥 </span>
</div>
</div>
</div>
</div>
{/* 浮动装饰元素 */}
<div className="absolute bottom-8 left-8 text-emerald-600 opacity-20 dark:opacity-10">
<div className="text-4xl">🔑</div>
</div>
<div className="absolute top-8 right-8 text-teal-500 opacity-20 dark:opacity-10">
<div className="text-4xl">🌟</div>
</div>
</div> </div>
); );
} }

View File

@@ -0,0 +1,164 @@
'use client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import Link from 'next/link';
import Image from 'next/image';
import { useSearchParams } from 'next/navigation';
export default function RegisterPage() {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl') || '/';
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800 relative overflow-hidden pb-8">
{/* 背景装饰元素 - 渐变模糊效果 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-20 -right-20 w-80 h-80 bg-emerald-400/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-20 -left-20 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl"></div>
<div className="absolute top-1/3 left-1/4 w-64 h-64 bg-emerald-300/10 rounded-full blur-3xl"></div>
</div>
{/* 主要内容区域 */}
<div className="relative z-10 flex flex-col items-center justify-center min-h-screen px-4 py-12">
<div className="w-full max-w-md">
{/* 装饰性头部 */}
<div className="text-center mb-8 sm:mb-10">
<div className="flex justify-center mb-6">
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 bg-emerald-600 rounded-2xl transform -rotate-6 shadow-lg transition-all duration-500 hover:rotate-0">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-white rounded-xl transform rotate-6 flex items-center justify-center transition-all duration-500 hover:rotate-0">
<Image
src="/images/mc-favicon.ico"
alt="Logo"
width={40}
height={40}
className="rounded-xl w-8 h-8"
/>
</div>
</div>
</div>
<h1 className="text-3xl sm:text-4xl font-bold text-gray-800 dark:text-white mb-3 tracking-tight">
<span className="text-emerald-600 dark:text-emerald-400"> HITWH</span>
</h1>
<p className="text-gray-600 dark:text-gray-400 text-sm sm:text-base leading-relaxed max-w-sm mx-auto">
Minecraft皮肤
</p>
</div>
{/* 注册卡片 */}
<div className="relative group">
{/* 卡片阴影装饰 */}
<div className="absolute inset-0 bg-emerald-500 rounded-2xl transform rotate-2 opacity-20 transition-all duration-500 group-hover:rotate-1 group-hover:scale-102"></div>
<div className="absolute inset-0 bg-teal-400 rounded-2xl transform -rotate-1 opacity-15 transition-all duration-500 group-hover:-rotate-0.5 group-hover:scale-101"></div>
<Card className="relative bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm border border-emerald-200 dark:border-emerald-900/50 shadow-xl rounded-2xl overflow-hidden">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardHeader className="text-center pb-6 pt-8">
<CardTitle className="text-xl sm:text-2xl font-bold text-gray-800 dark:text-white">
🎮
</CardTitle>
<div className="w-16 h-1 bg-gradient-to-r from-emerald-500 to-teal-500 mx-auto mt-2 rounded-full"></div>
</CardHeader>
<CardContent className="space-y-6 px-6 pb-8">
<form action="/api/auth/signup" method="post">
<input type="hidden" name="callbackUrl" value={callbackUrl} />
<div className="space-y-2">
<Label htmlFor="username" className="text-gray-700 dark:text-gray-300"></Label>
<Input
id="username"
name="username"
type="text"
placeholder="请输入用户名"
required
className="border-emerald-200 dark:border-emerald-900/30 focus:border-emerald-500 dark:focus:border-emerald-400 transition-all rounded-xl"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email" className="text-gray-700 dark:text-gray-300"></Label>
<Input
id="email"
name="email"
type="email"
placeholder="请输入您的邮箱"
required
className="border-emerald-200 dark:border-emerald-900/30 focus:border-emerald-500 dark:focus:border-emerald-400 transition-all rounded-xl"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-gray-700 dark:text-gray-300"></Label>
<Input
id="password"
name="password"
type="password"
placeholder="请输入密码"
required
className="border-emerald-200 dark:border-emerald-900/30 focus:border-emerald-500 dark:focus:border-emerald-400 transition-all rounded-xl"
/>
</div>
<Button
type="submit"
className="w-full mt-6 bg-emerald-600 hover:bg-emerald-700 text-white transition-all duration-300 transform hover:-translate-y-1 shadow-md hover:shadow-lg rounded-xl py-6"
>
</Button>
</form>
{/* 分割线 */}
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200 dark:border-gray-700"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-3 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded-full"></span>
</div>
</div>
{/* 登录链接 */}
<div className="text-center space-y-4">
<p className="text-gray-600 dark:text-gray-400">
</p>
<Button asChild variant="outline" className="w-full border-emerald-600 text-emerald-600 hover:bg-emerald-50 dark:border-emerald-400 dark:text-emerald-400 dark:hover:bg-emerald-900/20 rounded-xl py-6 transition-all duration-300 transform hover:-translate-y-1">
<Link href={`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`}>
🎮
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
{/* 底部装饰 */}
<div className="text-center mt-8 sm:mt-10">
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2 text-xs sm:text-sm text-gray-500 dark:text-gray-400">
<span>🎨 </span>
<span></span>
<span>🌍 </span>
<span></span>
<span> </span>
<span></span>
<span>👥 </span>
</div>
</div>
</div>
</div>
{/* 浮动装饰元素 */}
<div className="absolute bottom-8 left-8 text-emerald-600 opacity-20 dark:opacity-10">
<div className="text-4xl">🎨</div>
</div>
<div className="absolute top-8 right-8 text-teal-500 opacity-20 dark:opacity-10">
<div className="text-4xl">🌟</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,388 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import Canvas2DSkinPreview from '@/components/skins/Canvas2DSkinPreview';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import Link from 'next/link';
// 角色卡片组件
function CharacterCard({ character }: { character: any }) {
return (
<Card className="overflow-hidden transition-all duration-300 hover:shadow-xl bg-white dark:bg-gray-800 rounded-xl w-full flex flex-col md:flex-row border border-gray-100 dark:border-gray-700">
{/* 皮肤预览区域 */}
<div className="w-full md:w-1/5 aspect-video md:aspect-auto md:h-32 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/40 dark:to-indigo-900/40 flex items-center justify-center relative overflow-hidden">
{/* 装饰元素 */}
<div className="absolute -right-4 -bottom-4 w-20 h-20 bg-blue-200 dark:bg-blue-800/30 rounded-full opacity-50 blur-xl"></div>
<div className="absolute -left-4 -top-4 w-16 h-16 bg-indigo-200 dark:bg-indigo-800/30 rounded-full opacity-50 blur-xl"></div>
{/* 皮肤预览 - 使用Canvas2DSkinPreview组件 */}
<div className="relative z-10">
<Canvas2DSkinPreview
skinUrl={`/test-skin.png?skinId=${character.skinId}`}
size={112}
className="transition-transform duration-500 hover:scale-110"
/>
</div>
</div>
{/* 角色信息区域 */}
<div className="w-full md:w-4/5 p-6 flex flex-col justify-between">
<div>
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-3 mb-2">
<div>
<h3 className="text-2xl font-bold tracking-tight m-0">{character.name}</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{character.created}
</p>
</div>
<span className="bg-emerald-100 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300 px-3 py-1.5 rounded-full text-sm font-medium inline-block self-start">
{character.level}
</span>
</div>
{/* 额外的角色信息 */}
<div className="mt-3 flex flex-wrap gap-x-6 gap-y-2 text-sm text-gray-600 dark:text-gray-300">
<span className="flex items-center gap-1.5">
<span className="text-emerald-500"></span>
</span>
<span className="flex items-center gap-1.5">
<span className="text-blue-500"></span>
ID: {character.skinId}
</span>
</div>
</div>
{/* 操作按钮区域 */}
<div className="flex justify-end gap-3 mt-6">
<Button variant="ghost" className="rounded-lg px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
</Button>
<Button variant="ghost" className="rounded-lg px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
</Button>
<Button variant="ghost" className="rounded-lg px-4 py-2 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all">
</Button>
</div>
</div>
</Card>
);
}
// 添加角色卡片组件
function AddCharacterCard({ onAddClick }: { onAddClick: () => void }) {
return (
<Card
className="overflow-hidden border-2 border-dashed border-gray-300 dark:border-gray-700 hover:border-emerald-500 dark:hover:border-emerald-400 transition-all duration-300 cursor-pointer w-full flex items-center justify-center h-32 md:h-auto bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm"
onClick={onAddClick}
>
<div className="w-full h-32 flex flex-col items-center justify-center text-gray-500 dark:text-gray-400 group">
<div className="text-5xl mb-2 text-gray-400 dark:text-gray-600 group-hover:text-emerald-500 dark:group-hover:text-emerald-400 transition-all duration-300 transform group-hover:scale-110">+</div>
<span className="text-base font-medium group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors"></span>
</div>
</Card>
);
}
// 主客户端组件
export default function CharacterCenterClient({ userName, characters }: { userName: string; characters: any[] }) {
// 使用简单的状态管理来模拟标签页
const [activeTab, setActiveTab] = useState('my-characters');
// 角色创建表单状态
const [characterForm, setCharacterForm] = useState({
name: '',
description: '',
skinId: '',
capeId: '',
isActive: true,
});
// 生成UUID的简单方法不使用外部库
const generateUUID = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
// 生成RSA密钥的简单模拟实际项目中应在服务器端生成
const generateRSAKey = () => {
// 模拟RSA私钥实际项目中应使用加密库生成
return `-----BEGIN RSA PRIVATE KEY-----
MOCK-RSA-KEY-FOR-DEMO-PURPOSES-ONLY
-----END RSA PRIVATE KEY-----`;
};
// 处理表单输入变化
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setCharacterForm(prev => ({
...prev,
[name]: value
}));
};
// 处理复选框变化
const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCharacterForm(prev => ({
...prev,
isActive: e.target.checked
}));
};
// 处理皮肤选择
const handleSkinSelect = (skinId: string) => {
setCharacterForm(prev => ({
...prev,
skinId
}));
};
// 处理角色创建
const handleCreateCharacter = async () => {
if (!characterForm.name.trim()) {
alert('请输入角色名称');
return;
}
try {
// 生成角色UUID
const characterUuid = generateUUID();
// 生成RSA私钥
const rsaPrivateKey = generateRSAKey();
// 构建角色数据根据数据库profiles表结构
const newCharacter = {
uuid: characterUuid,
user_id: 'current-user-id', // 应从会话中获取真实用户ID
name: characterForm.name,
skin_id: characterForm.skinId || null,
cape_id: characterForm.capeId || null,
rsa_private_key: rsaPrivateKey,
is_active: characterForm.isActive,
description: characterForm.description
};
// 这里应该调用API发送到服务器
console.log('创建角色数据:', newCharacter);
// 模拟成功响应
alert('角色创建成功!\nUUID: ' + characterUuid.substring(0, 8) + '...');
// 重置表单
setCharacterForm({
name: '',
description: '',
skinId: '',
capeId: '',
isActive: true,
});
// 切换回我的角色标签
setActiveTab('my-characters');
} catch (error) {
console.error('创建角色失败:', error);
alert('创建角色失败,请重试');
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800 relative overflow-hidden text-gray-900 dark:text-gray-100">
{/* 背景装饰元素 - 渐变模糊效果 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-20 -right-20 w-80 h-80 bg-emerald-400/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-20 -left-20 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl"></div>
<div className="absolute top-1/3 left-1/4 w-64 h-64 bg-emerald-300/10 rounded-full blur-3xl"></div>
</div>
{/* 主要内容区域 */}
<main className="container mx-auto px-4 py-12 relative z-10">
{/* 页面标题区域 */}
<section className="mb-12 max-w-4xl mx-auto">
<div className="bg-gradient-to-r from-emerald-600 to-teal-500 rounded-2xl p-8 text-white shadow-xl transform transition-all duration-300 hover:shadow-2xl backdrop-blur-sm">
<h1 className="text-3xl md:text-4xl font-bold mb-3 tracking-tight"></h1>
<p className="text-base md:text-lg opacity-90 leading-relaxed">
{userName}
</p>
</div>
</section>
{/* 角色管理标签页 */}
<div className="w-full mb-16 max-w-4xl mx-auto">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
{/* 高级标签按钮组 */}
<div className="bg-white/80 dark:bg-gray-800/80 rounded-xl flex p-1.5 shadow-sm backdrop-blur-sm border border-emerald-100 dark:border-emerald-900/30">
<button
onClick={() => setActiveTab('my-characters')}
className={`px-6 py-2.5 rounded-lg transition-all duration-300 ${activeTab === 'my-characters' ? 'bg-white dark:bg-gray-700 font-medium shadow-md transform -translate-y-0.5' : 'hover:bg-gray-200 dark:hover:bg-gray-700/50'}`}
>
</button>
<button
onClick={() => setActiveTab('create-character')}
className={`px-6 py-2.5 rounded-lg transition-all duration-300 ${activeTab === 'create-character' ? 'bg-white dark:bg-gray-700 font-medium shadow-md transform -translate-y-0.5' : 'hover:bg-gray-200 dark:hover:bg-gray-700/50'}`}
>
</button>
</div>
<Button className="bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl px-6 py-2.5 shadow-md hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1">
<Link href="#"></Link>
</Button>
</div>
{/* 我的角色标签内容 */}
{activeTab === 'my-characters' && (
<div className="space-y-6">
<div className="flex flex-col gap-4">
{/* 角色卡片列表 */}
{characters.map((character) => (
<CharacterCard key={character.id} character={character} />
))}
{/* 添加新角色卡片 */}
<AddCharacterCard onAddClick={() => setActiveTab('create-character')} />
</div>
</div>
)}
{/* 创建角色标签内容 */}
{activeTab === 'create-character' && (
<div className="bg-white/95 dark:bg-gray-800/95 rounded-2xl p-8 shadow-xl border border-emerald-200 dark:border-emerald-900/30 backdrop-blur-sm">
<h3 className="text-2xl font-bold mb-8 tracking-tight"></h3>
<div className="max-w-2xl">
<div className="space-y-6">
<div className="space-y-3">
<Label htmlFor="character-name" className="text-base font-medium"> <span className="text-red-500">*</span></Label>
<Input
id="character-name"
name="name"
value={characterForm.name}
onChange={handleInputChange}
placeholder="输入Minecraft游戏内名称16个字符以内"
className="rounded-xl px-4 py-3 border-emerald-200 dark:border-emerald-900/30 focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all"
maxLength={16}
/>
<p className="text-xs text-gray-500 dark:text-gray-400">
Minecraft游戏内的用户名
</p>
</div>
<div className="space-y-3">
<Label className="text-base font-medium"></Label>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div
className={`border-2 rounded-xl p-3 cursor-pointer aspect-square bg-emerald-50 dark:bg-emerald-900/20 transition-all hover:shadow-xl transform hover:-translate-y-1 relative ${characterForm.skinId === 'skin1' ? 'border-emerald-500' : 'border-emerald-200/50 dark:border-emerald-900/10'}`}
onClick={() => handleSkinSelect('skin1')}
>
<Canvas2DSkinPreview
skinUrl="/test-skin.png"
size={128}
className="w-full h-full"
/>
{characterForm.skinId === 'skin1' && (
<div className="absolute top-3 right-3 w-3 h-3 bg-emerald-500 rounded-full"></div>
)}
</div>
<div
className={`border-2 rounded-xl p-3 cursor-pointer aspect-square transition-all hover:shadow-xl transform hover:-translate-y-1 relative ${characterForm.skinId === 'skin2' ? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20' : 'border-emerald-200/50 dark:border-emerald-900/10 hover:bg-emerald-50/50 dark:hover:bg-emerald-900/10'}`}
onClick={() => handleSkinSelect('skin2')}
>
<Canvas2DSkinPreview
skinUrl="/test-skin2.png"
size={128}
className="max-w-full max-h-full"
/>
{characterForm.skinId === 'skin2' && (
<div className="absolute top-3 right-3 w-3 h-3 bg-emerald-500 rounded-full"></div>
)}
</div>
<div
className={`border-2 rounded-xl p-3 cursor-pointer aspect-square transition-all hover:shadow-xl transform hover:-translate-y-1 relative ${characterForm.skinId === 'skin3' ? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20' : 'border-emerald-200/50 dark:border-emerald-900/10 hover:bg-emerald-50/50 dark:hover:bg-emerald-900/10'}`}
onClick={() => handleSkinSelect('skin3')}
>
<Canvas2DSkinPreview
skinUrl="/test-skin3.png"
size={128}
className="w-full h-full"
/>
{characterForm.skinId === 'skin3' && (
<div className="absolute top-3 right-3 w-3 h-3 bg-emerald-500 rounded-full"></div>
)}
</div>
</div>
</div>
<div className="space-y-3">
<Label htmlFor="character-description" className="text-base font-medium"></Label>
<Input
id="character-description"
name="description"
value={characterForm.description}
onChange={handleInputChange}
placeholder="描述你的角色..."
className="rounded-xl px-4 py-3 border-gray-200 dark:border-gray-700 focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all"
rows={4}
/>
</div>
<div className="flex items-center space-x-2 pt-2">
<input
id="is-active"
type="checkbox"
checked={characterForm.isActive}
onChange={handleCheckboxChange}
className="rounded border-gray-300 text-emerald-600 focus:ring-emerald-500"
/>
<Label htmlFor="is-active" className="text-sm"></Label>
</div>
<div className="bg-amber-50 dark:bg-amber-900/20 p-4 rounded-lg border border-amber-200 dark:border-amber-900/30">
<h4 className="font-medium text-amber-800 dark:text-amber-400 mb-2 flex items-center">
<span className="mr-2"></span>
</h4>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1.5">
<li> UUID</li>
<li> RSA密钥用于身份验证</li>
<li> Minecraft命名规范</li>
<li> 10</li>
</ul>
</div>
<Button
className="w-full bg-emerald-600 hover:bg-emerald-700 py-6 rounded-xl text-base font-medium shadow-md hover:shadow-lg transition-all transform hover:-translate-y-1"
onClick={handleCreateCharacter}
>
</Button>
</div>
</div>
</div>
)}
</div>
</main>
{/* 页脚 */}
<footer className="bg-gray-900/80 backdrop-blur-md text-gray-300 py-8 mt-16 border-t border-gray-800">
<div className="container mx-auto px-4 text-center relative z-10">
<p className="mb-2"> - Minecraft皮肤分享平台</p>
<p className="text-sm text-gray-400">© {new Date().getFullYear()} </p>
</div>
</footer>
</div>
);
}

View File

@@ -0,0 +1,110 @@
// src/app/character-center/page.tsx
// 角色中心页面 - 仅登录用户可访问
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/api/auth';
import { redirect } from 'next/navigation';
import CharacterCenterClient from './CharacterCenterClient';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
export default async function CharacterCenter() {
// 检查用户登录状态
const session = await getServerSession(authOptions);
if (!session) {
// 未登录用户重定向到登录页面
redirect('/');
}
// 安全地获取用户信息
const userName = session.user?.name || '玩家';
// 模拟的角色数据
const characters = [];
// 渲染客户端组件
return <CharacterCenterClient userName={userName} characters={characters} />;
}
// 角色卡片组件
function CharacterCard({ character }: { character: any }) {
return (
<Card className="overflow-hidden hover:shadow-lg transition-all duration-300 border-0 shadow-md">
<div className="relative">
{/* 角色状态标签 */}
<div className="absolute top-3 right-3 bg-green-500 text-white text-xs px-2 py-1 rounded-full">
</div>
{/* 角色皮肤预览 */}
<div className="h-48 bg-gradient-to-b from-blue-50 to-green-50 dark:from-gray-700 dark:to-gray-800 flex items-center justify-center p-4">
<div className="relative w-28 h-48">
<img
src="/test-skin.png"
alt={character.name}
className="object-contain w-full h-full"
/>
</div>
</div>
</div>
<CardHeader className="pb-2">
<CardTitle className="text-xl">{character.name}</CardTitle>
<div className="text-sm text-gray-500 dark:text-gray-400">
{character.level} {character.created}
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1"></div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full"
style={{ width: `${(character.level / 100) * 100}%` }}
></div>
</div>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between border-t pt-4">
<Button variant="ghost" size="sm" className="text-gray-600 dark:text-gray-300">
</Button>
<Button size="sm" className="bg-green-600 hover:bg-green-700">
</Button>
</CardFooter>
</Card>
);
}
// 添加角色卡片组件
function AddCharacterCard() {
return (
<Card className="overflow-hidden hover:shadow-lg transition-all duration-300 border-2 border-dashed border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 h-full flex flex-col items-center justify-center p-6 cursor-pointer">
<div className="text-6xl mb-4"></div>
<h3 className="text-lg font-bold text-gray-700 dark:text-gray-300 mb-2"></h3>
<p className="text-sm text-gray-500 dark:text-gray-400 text-center">
</p>
</Card>
);
}
// 统计卡片组件
function StatCard({ title, value, icon }: { title: string; value: string; icon: string }) {
return (
<Card className="border-0 shadow-md">
<CardHeader>
<div className="text-4xl mb-2">{icon}</div>
<CardTitle className="text-lg">{title}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">{value}</div>
</CardContent>
</Card>
);
}

View File

@@ -1,37 +1,176 @@
// src/app/dashboard/page.tsx // src/app/dashboard/page.tsx
import { getServerSession } from 'next-auth'; 'use client';
import { authOptions } from '@/lib/api/auth'; import { Button } from '@/components/ui/button';
import { redirect } from 'next/navigation'; import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
//import { Separator } from '@/components/ui/separator';
import Link from 'next/link';
import { useState } from 'react';
import Canvas2DSkinPreview from '@/components/skins/Canvas2DSkinPreview';
import SkinGrid from '@/components/skins/SkinGrid'; import SkinGrid from '@/components/skins/SkinGrid';
export default async function Dashboard() { export default function Dashboard() {
const session = await getServerSession(authOptions); // 由于这是客户端组件我们不能在这里使用getServerSession
// 在实际应用中你应该使用useSession钩子或在服务器端获取会话
if (!session) { // 这里我们模拟一个已登录的状态
redirect('/login'); const session = { user: { id: 'test_user_1', name: '测试玩家', email: 'test@test.com' } };
}
// 安全地获取用户ID // 安全地获取用户ID
const userId = session.user?.id || 'unknown'; const userId = session?.user?.id || 'unknown';
// 状态管理
const [searchTerm, setSearchTerm] = useState('');
const [activeCategory, setActiveCategory] = useState('all');
// 实际应用中这里会从API获取用户皮肤数据 // 实际应用中这里会从API获取用户皮肤数据
const mockSkins = [ const mockSkins = [
{ id: '1', name: 'Steve皮肤', createdAt: '2023-05-01' }, { id: '1', name: 'Steve皮肤', createdAt: '2023-05-01' },
{ id: '2', name: 'Alex皮肤', createdAt: '2023-05-15' }, { id: '2', name: 'Alex皮肤', createdAt: '2023-05-15' },
]; ];
// 根据搜索词和分类过滤皮肤
const filteredSkins = mockSkins.filter(skin =>
skin.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return ( return (
<div> <div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800 relative overflow-hidden text-gray-900 dark:text-gray-100">
<h1 className="text-3xl font-bold mb-6"></h1> {/* 背景装饰元素 - 渐变模糊效果 */}
<div className="mb-6"> <div className="absolute inset-0 overflow-hidden pointer-events-none">
<a <div className="absolute -top-20 -right-20 w-80 h-80 bg-emerald-400/20 rounded-full blur-3xl"></div>
href="/skins/upload" <div className="absolute -bottom-20 -left-20 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl"></div>
className="bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded" <div className="absolute top-1/3 left-1/4 w-64 h-64 bg-emerald-300/10 rounded-full blur-3xl"></div>
>
</a>
</div> </div>
<SkinGrid skins={mockSkins} />
{/* 主要内容区域 */}
<main className="container mx-auto px-4 py-12 relative z-10">
{/* 页面标题和操作按钮 */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-10 gap-4">
<div>
<h1 className="text-3xl font-bold mb-2 text-gray-800 dark:text-white"></h1>
<p className="text-gray-600 dark:text-gray-400">Minecraft皮肤</p>
</div>
<Button asChild className="bg-emerald-600 hover:bg-emerald-700 text-white transition-all duration-300 transform hover:-translate-y-1 shadow-md hover:shadow-lg">
<Link href="/skins/upload">
<span className="mr-2">📤</span>
</Link>
</Button>
</div>
{/* 搜索栏 */}
<div className="mb-8 max-w-4xl mx-auto">
<div className="relative">
<Input
type="text"
placeholder="搜索你的皮肤..."
className="pl-10 pr-4 py-2 w-full border-emerald-200 dark:border-emerald-900/30 focus:border-emerald-500 dark:focus:border-emerald-400 rounded-xl shadow-sm transition-all bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400">
🔍
</div>
</div>
</div>
{/* 皮肤分类标签 */}
<div className="flex overflow-x-auto gap-3 pb-4 mb-8 scrollbar-hide max-w-4xl mx-auto">
<Button
variant={activeCategory === 'all' ? 'default' : 'ghost'}
className={`rounded-full ${activeCategory === 'all' ? 'bg-emerald-600 hover:bg-emerald-700 text-white shadow-md' : 'hover:text-emerald-600 dark:hover:text-emerald-400'} transition-all`}
onClick={() => setActiveCategory('all')}
>
</Button>
<Button
variant={activeCategory === 'recent' ? 'default' : 'ghost'}
className={`rounded-full ${activeCategory === 'recent' ? 'bg-emerald-600 hover:bg-emerald-700 text-white shadow-md' : 'hover:text-emerald-600 dark:hover:text-emerald-400'} transition-all`}
onClick={() => setActiveCategory('recent')}
>
</Button>
<Button
variant={activeCategory === 'popular' ? 'default' : 'ghost'}
className={`rounded-full ${activeCategory === 'popular' ? 'bg-emerald-600 hover:bg-emerald-700 text-white shadow-md' : 'hover:text-emerald-600 dark:hover:text-emerald-400'} transition-all`}
onClick={() => setActiveCategory('popular')}
>
</Button>
<Button
variant={activeCategory === 'favorites' ? 'default' : 'ghost'}
className={`rounded-full ${activeCategory === 'favorites' ? 'bg-emerald-600 hover:bg-emerald-700 text-white shadow-md' : 'hover:text-emerald-600 dark:hover:text-emerald-400'} transition-all`}
onClick={() => setActiveCategory('favorites')}
>
</Button>
</div>
{/* 皮肤网格 */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 max-w-6xl mx-auto">
{filteredSkins.map((skin) => (
<Card key={skin.id} className="overflow-hidden border border-emerald-200 dark:border-emerald-900/30 rounded-2xl hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm">
<div className="aspect-square bg-gray-100/95 dark:bg-gray-900/95 flex items-center justify-center p-4 relative overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-emerald-50/50 to-teal-50/50 dark:from-emerald-900/10 dark:to-teal-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<Canvas2DSkinPreview
skinUrl={`/test-skin.png`}
size={128}
className="max-w-full max-h-full relative z-10 transition-transform duration-500 group-hover:scale-110"
/>
</div>
<CardContent className="p-5">
<div className="flex justify-between items-start mb-2">
<h3 className="font-semibold text-lg text-gray-800 dark:text-white truncate group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors">{skin.name}</h3>
<Button variant="ghost" size="icon" className="h-8 w-8 text-gray-500 hover:text-rose-500 dark:hover:text-rose-400 rounded-full">
</Button>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4"> {skin.createdAt}</p>
<div className="flex gap-2">
<Button size="sm" variant="ghost" className="text-xs flex-1 border border-emerald-200 dark:border-emerald-900/30 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-lg">
</Button>
<Button size="sm" variant="ghost" className="text-xs flex-1 border border-emerald-200 dark:border-emerald-900/30 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-lg">
</Button>
<Button size="sm" variant="ghost" className="text-xs flex-1 border border-emerald-200 dark:border-emerald-900/30 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-lg">
</Button>
</div>
</CardContent>
</Card>
))}
</div>
{/* 空状态 */}
{filteredSkins.length === 0 && (
<div className="text-center py-16 max-w-4xl mx-auto">
<Card className="border border-emerald-200 dark:border-emerald-900/30 rounded-2xl overflow-hidden bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm shadow-xl">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardContent className="p-8">
<div className="text-6xl mb-4">📦</div>
<h3 className="text-xl font-semibold mb-2 text-gray-800 dark:text-white"></h3>
<p className="text-gray-600 dark:text-gray-400 mb-6 max-w-md mx-auto">
Minecraft皮肤收藏之旅
</p>
<Button asChild className="bg-emerald-600 hover:bg-emerald-700 text-white transition-all duration-300 transform hover:-translate-y-1 shadow-md hover:shadow-lg">
<Link href="/skins/upload">
<span className="mr-2">📤</span>
</Link>
</Button>
</CardContent>
</Card>
</div>
)}
</main>
{/* 页脚 */}
<footer className="bg-gray-900/80 backdrop-blur-md text-gray-300 py-8 mt-16 border-t border-gray-800">
<div className="container mx-auto px-4 text-center relative z-10">
<p className="mb-2"> - Minecraft皮肤分享平台</p>
<p className="text-sm text-gray-400">© {new Date().getFullYear()} </p>
</div>
</footer>
</div> </div>
); );
} }

View File

@@ -117,6 +117,80 @@
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground font-sans;
background-image:
linear-gradient(45deg, #e6f7ef 25%, transparent 25%),
linear-gradient(-45deg, #e6f7ef 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #e6f7ef 75%),
linear-gradient(-45deg, transparent 75%, #e6f7ef 75%);
background-size: 40px 40px;
background-position:
0 0,
0 20px,
20px -20px,
-20px 0px;
}
.dark body {
background-image:
linear-gradient(45deg, rgba(16, 185, 129, 0.1) 25%, transparent 25%),
linear-gradient(-45deg, rgba(16, 185, 129, 0.1) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgba(16, 185, 129, 0.1) 75%),
linear-gradient(-45deg, transparent 75%, rgba(16, 185, 129, 0.1) 75%);
}
}
@layer utilities {
.content-auto {
content-visibility: auto;
}
/* 响应式间距工具 */
.spacing-sm {
@apply p-2 sm:p-3 md:p-4;
}
.spacing-md {
@apply p-3 sm:p-4 md:p-6;
}
.spacing-lg {
@apply p-4 sm:p-6 md:p-8;
}
/* 响应式字体大小 */
.text-responsive {
@apply text-base sm:text-lg md:text-xl;
}
/* 响应式网格布局 */
.grid-responsive {
@apply grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4;
}
/* 移动端安全区域padding */
.safe-area-inset {
@apply px-4 sm:px-6;
}
/* 修复iOS上的点击延迟 */
.ios-tap {
-webkit-tap-highlight-color: transparent;
}
/* 登录弹窗动画 */
.animate-fadeIn {
animation: fadeIn 0.3s ease-in-out;
}
.animate-slideUp {
animation: slideUp 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
} }

182
src/app/help/basic/page.tsx Normal file
View File

@@ -0,0 +1,182 @@
// src/app/help/basic/page.tsx
// 基础教程页面
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
export default function BasicTutorialPage() {
return (
<main className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 py-12">
<div className="container mx-auto px-4 max-w-4xl">
{/* 页面导航 */}
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-6">
<Link href="/" className="hover:text-blue-600 dark:hover:text-blue-400"></Link>
<span>/</span>
<Link href="/help" className="hover:text-blue-600 dark:hover:text-blue-400"></Link>
<span>/</span>
<span className="text-blue-600 dark:text-blue-400"></span>
</div>
{/* 页面标题 */}
<div className="mb-12">
<h1 className="text-3xl font-bold text-blue-700 dark:text-blue-400 mb-4"></h1>
<p className="text-xl text-gray-600 dark:text-gray-400">
使
</p>
</div>
{/* 教程内容列表 */}
<div className="space-y-8">
{/* 教程项1 */}
<Card className="overflow-hidden border-blue-200 dark:border-blue-900/50">
<CardHeader className="bg-blue-50 dark:bg-blue-900/20">
<CardTitle className="text-xl text-blue-700 dark:text-blue-400">1. </CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-4">
<p className="text-gray-600 dark:text-gray-400">
使"注册"使
</p>
<div className="bg-gray-50 dark:bg-gray-900/50 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 className="font-semibold text-gray-800 dark:text-white mb-2"></h4>
<p className="text-gray-600 dark:text-gray-400">
使
</p>
</div>
</div>
</CardContent>
</Card>
{/* 教程项2 */}
<Card className="overflow-hidden border-blue-200 dark:border-blue-900/50">
<CardHeader className="bg-blue-50 dark:bg-blue-900/20">
<CardTitle className="text-xl text-blue-700 dark:text-blue-400">2. </CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-4">
<p className="text-gray-600 dark:text-gray-400">
"上传皮肤""选择文件"PNG格式的皮肤文件"上传"
</p>
<div className="bg-amber-50 dark:bg-amber-900/20 p-4 rounded-lg border border-amber-200 dark:border-amber-900/50">
<h4 className="font-semibold text-amber-700 dark:text-amber-400 mb-2"></h4>
<ul className="list-disc list-inside space-y-1 text-gray-600 dark:text-gray-400">
<li>PNG格式</li>
<li>Minecraft标准64x32或64x64像素</li>
<li>使</li>
</ul>
</div>
</div>
</CardContent>
</Card>
{/* 教程项3 */}
<Card className="overflow-hidden border-blue-200 dark:border-blue-900/50">
<CardHeader className="bg-blue-50 dark:bg-blue-900/20">
<CardTitle className="text-xl text-blue-700 dark:text-blue-400">3. </CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-gray-600 dark:text-gray-400 mb-4">
"个人中心"
</p>
<ul className="list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4 mb-4">
<li>使</li>
<li></li>
<li></li>
<li>使</li>
</ul>
</CardContent>
</Card>
{/* 教程项4 */}
<Card className="overflow-hidden border-blue-200 dark:border-blue-900/50">
<CardHeader className="bg-blue-50 dark:bg-blue-900/20">
<CardTitle className="text-xl text-blue-700 dark:text-blue-400">4. 使</CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-gray-600 dark:text-gray-400 mb-4">
"下载皮肤"使
</p>
<ol className="list-decimal list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4 mb-6">
<li>Minecraft启动器</li>
<li>"皮肤"</li>
<li>"浏览"</li>
<li>"保存"</li>
</ol>
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-900/50">
<p className="text-gray-600 dark:text-gray-400">
<strong></strong> 使Yggdrasil验证Yggdrasil教程了解更多信息
</p>
</div>
</CardContent>
</Card>
{/* 教程项5 */}
<Card className="overflow-hidden border-blue-200 dark:border-blue-900/50">
<CardHeader className="bg-blue-50 dark:bg-blue-900/20">
<CardTitle className="text-xl text-blue-700 dark:text-blue-400">5. Yggdrasil验证简介</CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-gray-600 dark:text-gray-400 mb-4">
HITWH.GAMES平台提供了Yggdrasil验证服务Minecraft身份验证协议的系统使
</p>
<div className="space-y-4 mb-6">
<h4 className="font-semibold text-gray-800 dark:text-white">Yggdrasil验证</h4>
<ul className="list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4">
<li>Minecraft服务器上显示您的自定义皮肤</li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg border border-green-200 dark:border-green-900/50">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<p className="text-gray-600 dark:text-gray-400">
便<span className="font-semibold"></span>HMCLPCLYggdrasil服务配置Yggdrasil教程
</p>
</div>
</div>
</CardContent>
</Card>
{/* 教程项6 */}
<Card className="overflow-hidden border-blue-200 dark:border-blue-900/50">
<CardHeader className="bg-blue-50 dark:bg-blue-900/20">
<CardTitle className="text-xl text-blue-700 dark:text-blue-400">6. </CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-gray-600 dark:text-gray-400 mb-4">
Minecraft玩家社区
</p>
<ul className="list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</CardContent>
</Card>
</div>
{/* 页面导航按钮 */}
<div className="flex justify-between items-center mt-12">
<Link href="/help">
<Button variant="ghost" className="text-gray-600 dark:text-gray-400">
</Button>
</Link>
<Link href="/help/yggdrasil">
<Button className="bg-purple-600 hover:bg-purple-700 text-white">
Yggdrasil教程
</Button>
</Link>
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,196 @@
// src/app/help/customskinloader/page.tsx
// CustomSkinLoader教程页面
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
export default function CustomSkinLoaderTutorialPage() {
return (
<main className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 py-12">
<div className="container mx-auto px-4 max-w-4xl">
{/* 页面导航 */}
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-6">
<Link href="/" className="hover:text-green-600 dark:hover:text-green-400"></Link>
<span>/</span>
<Link href="/help" className="hover:text-green-600 dark:hover:text-green-400"></Link>
<span>/</span>
<span className="text-green-600 dark:text-green-400">CustomSkinLoader教程</span>
</div>
{/* 页面标题 */}
<div className="mb-12">
<h1 className="text-3xl font-bold text-green-700 dark:text-green-400 mb-4">CustomSkinLoader教程</h1>
<p className="text-xl text-gray-600 dark:text-gray-400">
CustomSkinLoader
</p>
</div>
{/* 教程内容列表 */}
<div className="space-y-8">
{/* 教程项1 */}
<Card className="overflow-hidden border-green-200 dark:border-green-900/50">
<CardHeader className="bg-green-50 dark:bg-green-900/20">
<CardTitle className="text-xl text-green-700 dark:text-green-400">1. CustomSkinLoader</CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-gray-600 dark:text-gray-400 mb-4">
CustomSkinLoader是一个Minecraft客户端模组Mojang官方服务器的情况下加载自定义皮肤使线CustomSkinLoader都能帮助您显示自己喜欢的皮肤
</p>
<div className="bg-cyan-50 dark:bg-cyan-900/20 p-4 rounded-lg border border-cyan-200 dark:border-cyan-900/50">
<h4 className="font-semibold text-cyan-700 dark:text-cyan-400 mb-2"></h4>
<ul className="list-disc list-inside space-y-1 text-gray-600 dark:text-gray-400">
<li></li>
<li>线</li>
<li></li>
<li></li>
</ul>
</div>
</CardContent>
</Card>
{/* 教程项2 */}
<Card className="overflow-hidden border-green-200 dark:border-green-900/50">
<CardHeader className="bg-green-50 dark:bg-green-900/20">
<CardTitle className="text-xl text-green-700 dark:text-green-400">2. CustomSkinLoader</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-4">
<h4 className="font-semibold text-gray-800 dark:text-white">Forge/Fabric安装方法</h4>
<ol className="list-decimal list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4">
<li>Minecraft Forge或Fabric</li>
<li>Minecraft版本兼容的CustomSkinLoader模组文件.jar格式</li>
<li>Minecraft的mods文件夹中
<ul className="list-disc list-inside ml-8 mt-2">
<li>Windows: <code className="bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">%appdata%\.minecraft\mods</code></li>
<li>macOS: <code className="bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">~/Library/Application Support/minecraft/mods</code></li>
<li>Linux: <code className="bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">~/.minecraft/mods</code></li>
</ul>
</li>
<li>Minecraft</li>
</ol>
<h4 className="font-semibold text-gray-800 dark:text-white">LiteLoader安装方法</h4>
<ol className="list-decimal list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4">
<li>LiteLoader</li>
<li>CustomSkinLoader的LiteLoader版本</li>
<li>liteloader文件夹中</li>
<li>Minecraft</li>
</ol>
</div>
</CardContent>
</Card>
{/* 教程项3 */}
<Card className="overflow-hidden border-green-200 dark:border-green-900/50">
<CardHeader className="bg-green-50 dark:bg-green-900/20">
<CardTitle className="text-xl text-green-700 dark:text-green-400">3. OurSkin服务</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-4">
<p className="text-gray-600 dark:text-gray-400">
CustomSkinLoader以使用我们的皮肤服务
</p>
<ol className="list-decimal list-inside space-y-3 text-gray-600 dark:text-gray-400 ml-4">
<li>Minecraft一次</li>
<li>MinecraftCustomSkinLoader的配置文件
<ul className="list-disc list-inside ml-8 mt-2">
<li><code className="bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">.minecraft/config/CustomSkinLoader/CustomSkinLoader.json</code></li>
</ul>
</li>
<li>使</li>
<li></li>
</ol>
<div className="bg-gray-50 dark:bg-gray-900/50 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 className="font-semibold text-gray-800 dark:text-white mb-2"></h4>
<pre className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm">
{`{
"enable": true,
"loadlist": [
{
"name": "OurSkin",
"type": "CustomSkinAPI",
"root": "https://example.com/api/csl/"
},
{
"name": "Mojang",
"type": "MojangAPI",
"root": "https://api.mojang.com/"
}
],
"disableHttpCheck": false,
"disableSkinLoadLogging": false
}`}
</pre>
</div>
</div>
</CardContent>
</Card>
{/* 教程项4 */}
<Card className="overflow-hidden border-green-200 dark:border-green-900/50">
<CardHeader className="bg-green-50 dark:bg-green-900/20">
<CardTitle className="text-xl text-green-700 dark:text-green-400">4. </CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-4">
<h4 className="font-semibold text-gray-800 dark:text-white"></h4>
<p className="text-gray-600 dark:text-gray-400">
CustomSkinLoader会按照配置文件中服务的顺序尝试加载皮肤使
</p>
<h4 className="font-semibold text-gray-800 dark:text-white"></h4>
<div className="space-y-3">
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-900/50">
<h5 className="font-semibold text-red-700 dark:text-red-400 mb-1"></h5>
<p className="text-gray-600 dark:text-gray-400 text-sm">
URL是否正确F5刷新皮肤
</p>
</div>
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-900/50">
<h5 className="font-semibold text-red-700 dark:text-red-400 mb-1"></h5>
<p className="text-gray-600 dark:text-gray-400 text-sm">
Minecraft版本兼容
</p>
</div>
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-900/50">
<h5 className="font-semibold text-red-700 dark:text-red-400 mb-1"></h5>
<p className="text-gray-600 dark:text-gray-400 text-sm">
JSON格式Minecraft
</p>
</div>
</div>
<h4 className="font-semibold text-gray-800 dark:text-white"></h4>
<p className="text-gray-600 dark:text-gray-400">
使CustomSkinLoader
</p>
<ul className="list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4">
<li><code className="bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">/customskinloader reload</code> - </li>
<li><code className="bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">/customskinloader setskin [username] [skinUrl]</code> - </li>
<li><code className="bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">/customskinloader status</code> - </li>
</ul>
</div>
</CardContent>
</Card>
</div>
{/* 页面导航按钮 */}
<div className="flex justify-between items-center mt-12">
<Link href="/help/multilogin">
<Button variant="ghost" className="text-gray-600 dark:text-gray-400">
MultiLogin教程
</Button>
</Link>
<Link href="/help">
<Button variant="default" className="bg-emerald-600 hover:bg-emerald-700 text-white">
</Button>
</Link>
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,245 @@
// src/app/help/multilogin/page.tsx
// MultiLogin教程页面
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
export default function MultiLoginTutorialPage() {
return (
<main className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 py-12">
<div className="container mx-auto px-4 max-w-4xl">
{/* 页面导航 */}
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-6">
<Link href="/" className="hover:text-amber-600 dark:hover:text-amber-400"></Link>
<span>/</span>
<Link href="/help" className="hover:text-amber-600 dark:hover:text-amber-400"></Link>
<span>/</span>
<span className="text-amber-600 dark:text-amber-400">MultiLogin教程</span>
</div>
{/* 页面标题 */}
<div className="mb-12">
<h1 className="text-3xl font-bold text-amber-700 dark:text-amber-400 mb-4">MultiLogin教程</h1>
<p className="text-xl text-gray-600 dark:text-gray-400">
MultiLogin来支持多种登录方式共存
</p>
</div>
{/* 教程内容列表 */}
<div className="space-y-8">
{/* 什么是MultiLogin */}
<Card className="overflow-hidden border-amber-200 dark:border-amber-900/50">
<CardHeader className="bg-amber-50 dark:bg-amber-900/20">
<CardTitle className="text-xl text-amber-700 dark:text-amber-400">MultiLogin</CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-gray-600 dark:text-gray-400 mb-6">
MultiLogin
</p>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Minecraft 便使MultiLogin
</p>
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg border border-yellow-200 dark:border-yellow-900/50 mb-6">
<h4 className="font-semibold text-yellow-700 dark:text-yellow-400 mb-2"> </h4>
<p className="text-gray-600 dark:text-gray-400">
Minecraft使 Minecraft
</p>
</div>
<div className="bg-red-50 dark:bg-red-900/20 p-4 rounded-lg border border-red-200 dark:border-red-900/50">
<h4 className="font-semibold text-red-700 dark:text-red-400 mb-2"></h4>
<p className="text-gray-600 dark:text-gray-400">
MultiLogin 0.6.12 BukkitBungee Velocity
</p>
</div>
</CardContent>
</Card>
{/* Velocity配置 */}
<Card className="overflow-hidden border-amber-200 dark:border-amber-900/50">
<CardHeader className="bg-amber-50 dark:bg-amber-900/20">
<CardTitle className="text-xl text-amber-700 dark:text-amber-400">Velocity配置 (Minecraft 1.13+)</CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-gray-600 dark:text-gray-400 mb-6">
Modern forwarding Velocity Minecraft 1.13
</p>
<p className="text-gray-600 dark:text-gray-400 mb-6">
使 Velocity + Paper + MultiLogin Velocity Velocity
</p>
<div className="space-y-8">
<div>
<h4 className="text-xl font-semibold text-gray-800 dark:text-white mb-4">1. Velocity </h4>
<div className="space-y-6">
<div>
<h5 className="font-semibold text-gray-700 dark:text-gray-300 mb-3"> Velocity</h5>
<p className="text-gray-600 dark:text-gray-400 mb-3">
velocity.toml online-mode true 👈
</p>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm">
<pre>{`# velocity.toml
# Should we authenticate players with Mojang? By default, this is on.
online-mode = true`}</pre>
</div>
</div>
<div>
<h5 className="font-semibold text-gray-700 dark:text-gray-300 mb-3"> Paper </h5>
<ul className="list-disc list-inside space-y-3 text-gray-600 dark:text-gray-400 mb-4">
<li> server.properties online-mode false 👈<br/>
Velocity </li>
<li> config/paper-global.yaml online-mode true 👈<br/>
velocity.toml online-mode </li>
<li> Paper 1.18.2 online-mode settings.velocity-support.online-mode</li>
</ul>
<div className="space-y-4">
<div>
<p className="text-sm text-gray-500 dark:text-gray-500 mb-1">server.properties</p>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm">
<pre>online-mode=false</pre>
</div>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-500 mb-1">config/paper-global.yaml</p>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm">
<pre>online-mode: true</pre>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<h4 className="text-xl font-semibold text-gray-800 dark:text-white mb-4">2. MultiLogin</h4>
<div className="space-y-6">
<div>
<h5 className="font-semibold text-gray-700 dark:text-gray-300 mb-3"> Velocity</h5>
<p className="text-gray-600 dark:text-gray-400 mb-4">
MultiLogin
</p>
<ol className="list-decimal list-inside space-y-3 text-gray-600 dark:text-gray-400 mb-4">
<li> Mojang和LittleSkin </li>
<ul className="list-disc list-inside ml-8 mt-2 text-gray-600 dark:text-gray-400">
<li>multilogin/services/offical.yml</li>
<li>multilogin/services/littleskin.yml</li>
</ul>
</ol>
<div className="space-y-6">
<div>
<p className="text-sm text-gray-500 dark:text-gray-500 mb-1">multilogin/services/offical.yml</p>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm">
<pre>{`# 官方验证服务配置
# Below, only the most basic configuration is provided.
# You can refer to the template file to complete all configurations.
# Please edit before use.
id: 0
name: 'Official'
# Don't change it unless you really want to.
serviceType: OFFICIAL`}</pre>
</div>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-500 mb-1">multilogin/services/littleskin.yml</p>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm">
<pre>{`# LittleSkin验证服务配置
# Below, only the most basic configuration is provided.
# You can refer to the template file to complete all configurations.
# Please edit before use.
id: 1
name: 'LittleSkin'
# Don't change it unless you really want to.
serviceType: BLESSING_SKIN
# Fill in the API root like \`https://skin.example.com/api/yggdrasil\`
apiRoot: 'https://skin.littlelan.cn/api/yggdrasil'`}</pre>
</div>
</div>
</div>
</div>
<div>
<h5 className="font-semibold text-gray-700 dark:text-gray-300 mb-3"></h5>
<p className="text-gray-600 dark:text-gray-400">
Velocity MultiLogin
</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 更多资源 */}
<Card className="overflow-hidden border-amber-200 dark:border-amber-900/50">
<CardHeader className="bg-amber-50 dark:bg-amber-900/20">
<CardTitle className="text-xl text-amber-700 dark:text-amber-400"></CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-gray-600 dark:text-gray-400 mb-4">
MultiLogin 使
</p>
<ul className="space-y-3">
<li>
<a href="https://github.com/CaaMoe/MultiLogin/wiki" target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline flex items-center">
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm6.066 9.645c.183 4.04-2.83 8.544-8.164 8.544-1.622 0-3.131-.476-4.402-1.291 1.524.18 3.045-.244 4.252-1.189-1.256-.023-2.317-.854-2.684-1.995.451.086.895.061 1.298-.049-1.381-.278-2.335-1.522-2.304-2.853.388.215.83.344 1.301.359-1.279-.855-1.641-2.544-.889-3.835 1.416 1.738 3.533 2.881 5.92 3.001-.419-1.796.944-3.527 2.799-3.527.825 0 1.572.349 2.096.907.654-.128 1.27-.368 1.824-.697-.215.671-.67 1.233-1.263 1.589.581-.07 1.135-.224 1.649-.453-.384.578-.87 1.084-1.433 1.489z"/>
</svg>
MultiLogin GitHub Wiki
</a>
</li>
<li>
<a href="https://docs.papermc.io/velocity" target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline flex items-center">
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm6.066 9.645c.183 4.04-2.83 8.544-8.164 8.544-1.622 0-3.131-.476-4.402-1.291 1.524.18 3.045-.244 4.252-1.189-1.256-.023-2.317-.854-2.684-1.995.451.086.895.061 1.298-.049-1.381-.278-2.335-1.522-2.304-2.853.388.215.83.344 1.301.359-1.279-.855-1.641-2.544-.889-3.835 1.416 1.738 3.533 2.881 5.92 3.001-.419-1.796.944-3.527 2.799-3.527.825 0 1.572.349 2.096.907.654-.128 1.27-.368 1.824-.697-.215.671-.67 1.233-1.263 1.589.581-.07 1.135-.224 1.649-.453-.384.578-.87 1.084-1.433 1.489z"/>
</svg>
Velocity
</a>
</li>
<li>
<a href="https://github.com/yushijinhun/authlib-injector/wiki" target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline flex items-center">
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm6.066 9.645c.183 4.04-2.83 8.544-8.164 8.544-1.622 0-3.131-.476-4.402-1.291 1.524.18 3.045-.244 4.252-1.189-1.256-.023-2.317-.854-2.684-1.995.451.086.895.061 1.298-.049-1.381-.278-2.335-1.522-2.304-2.853.388.215.83.344 1.301.359-1.279-.855-1.641-2.544-.889-3.835 1.416 1.738 3.533 2.881 5.92 3.001-.419-1.796.944-3.527 2.799-3.527.825 0 1.572.349 2.096.907.654-.128 1.27-.368 1.824-.697-.215.671-.67 1.233-1.263 1.589.581-.07 1.135-.224 1.649-.453-.384.578-.87 1.084-1.433 1.489z"/>
</svg>
authlib-injector Wiki
</a>
</li>
</ul>
</CardContent>
</Card>
</div>
{/* 页面导航按钮 */}
<div className="flex justify-between items-center mt-12">
<Link href="/help/yggdrasil">
<Button variant="ghost" className="text-gray-600 dark:text-gray-400">
Yggdrasil教程
</Button>
</Link>
<Link href="/help/customskinloader">
<Button className="bg-green-600 hover:bg-green-700 text-white">
CustomSkinLoader教程
</Button>
</Link>
</div>
</div>
</main>
);
}

103
src/app/help/page.tsx Normal file
View File

@@ -0,0 +1,103 @@
// src/app/help/page.tsx
// 帮助页面主入口
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
export default function HelpPage() {
return (
<main className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 py-12">
<div className="container mx-auto px-4 max-w-4xl">
{/* 页面标题 */}
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-800 dark:text-white mb-4"></h1>
<p className="text-xl text-gray-600 dark:text-gray-400">
使
</p>
</div>
{/* 教程分类卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Link href="/help/basic" className="block">
<Card className="h-full overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 border-blue-200 dark:border-blue-900/50">
<CardHeader className="bg-blue-50 dark:bg-blue-900/20">
<CardTitle className="text-blue-700 dark:text-blue-400"></CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 dark:text-gray-400">
</p>
<Button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white">
</Button>
</CardContent>
</Card>
</Link>
<Link href="/help/yggdrasil" className="block">
<Card className="h-full overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 border-purple-200 dark:border-purple-900/50">
<CardHeader className="bg-purple-50 dark:bg-purple-900/20">
<CardTitle className="text-purple-700 dark:text-purple-400">Yggdrasil教程</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 dark:text-gray-400">
使Yggdrasil验证服务
</p>
<Button className="mt-4 bg-purple-600 hover:bg-purple-700 text-white">
</Button>
</CardContent>
</Card>
</Link>
<Link href="/help/multilogin" className="block">
<Card className="h-full overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 border-amber-200 dark:border-amber-900/50">
<CardHeader className="bg-amber-50 dark:bg-amber-900/20">
<CardTitle className="text-amber-700 dark:text-amber-400">MultiLogin教程</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 dark:text-gray-400">
MultiLogin插件的配置与使用方法
</p>
<Button className="mt-4 bg-amber-600 hover:bg-amber-700 text-white">
</Button>
</CardContent>
</Card>
</Link>
<Link href="/help/customskinloader" className="block">
<Card className="h-full overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 border-green-200 dark:border-green-900/50">
<CardHeader className="bg-green-50 dark:bg-green-900/20">
<CardTitle className="text-green-700 dark:text-green-400">CustomSkinLoader教程</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 dark:text-gray-400">
CustomSkinLoader
</p>
<Button className="mt-4 bg-green-600 hover:bg-green-700 text-white">
</Button>
</CardContent>
</Card>
</Link>
</div>
{/* 其他资源 */}
<div className="mt-16 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl p-8 shadow-lg border border-gray-200 dark:border-gray-700">
<h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-6"></h2>
<div className="space-y-4">
<p className="text-gray-600 dark:text-gray-400">
使
</p>
<ul className="list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4">
<li> <Link href="#" className="text-blue-600 dark:text-blue-400 hover:underline"></Link></li>
<li> <Link href="#" className="text-blue-600 dark:text-blue-400 hover:underline"></Link></li>
<li> <Link href="#" className="text-blue-600 dark:text-blue-400 hover:underline"></Link> </li>
</ul>
</div>
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,51 @@
// src/app/help/yggdrasil/DragConfigCard.tsx
'use client'; // 标记为客户端组件以支持交互功能
import { useState } from 'react';
interface DragConfigCardProps {
authUrl: string;
serviceName: string;
}
export default function DragConfigCard({ authUrl, serviceName }: DragConfigCardProps) {
const [isDragging, setIsDragging] = useState(false);
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
// 创建适合HMCL/PCL启动器的Yggdrasil配置链接
const yggdrasilUrl = `authlib-injector:yggdrasil-api?url=${encodeURIComponent(authUrl)}&name=${encodeURIComponent(serviceName)}`;
e.dataTransfer.setData('text/plain', yggdrasilUrl);
setIsDragging(true);
};
const handleDragEnd = () => {
setIsDragging(false);
};
return (
<div
className={`bg-green-50 dark:bg-green-900/20 p-6 rounded-lg border-2 border-dashed border-green-300 dark:border-green-700 mb-8 cursor-move hover:bg-green-100 dark:hover:bg-green-900/30 transition-colors ${isDragging ? 'opacity-50' : 'opacity-100'}`}
draggable="true"
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex items-center justify-center gap-4">
<svg className="w-10 h-10 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<div>
<h4 className="font-bold text-green-700 dark:text-green-400">{serviceName}</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">HMCL/PCL启动器以自动配置</p>
</div>
</div>
<div className="mt-4 p-3 bg-white/50 dark:bg-gray-800/50 rounded-md text-sm text-gray-700 dark:text-gray-300">
<p className="flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Yggdrasil服务器地址和名称
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,200 @@
// src/app/help/yggdrasil/page.tsx
// Yggdrasil教程页面
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import DragConfigCard from './DragConfigCard';
export default function YggdrasilTutorialPage() {
return (
<main className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 py-12">
<div className="container mx-auto px-4 max-w-4xl">
{/* 页面导航 */}
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-6">
<Link href="/" className="hover:text-purple-600 dark:hover:text-purple-400"></Link>
<span>/</span>
<Link href="/help" className="hover:text-purple-600 dark:hover:text-purple-400"></Link>
<span>/</span>
<span className="text-purple-600 dark:text-purple-400">Yggdrasil教程</span>
</div>
{/* 页面标题 */}
<div className="mb-12">
<h1 className="text-3xl font-bold text-purple-700 dark:text-purple-400 mb-4">Yggdrasil教程</h1>
<p className="text-xl text-gray-600 dark:text-gray-400">
使Yggdrasil验证服务
</p>
</div>
{/* 教程内容列表 */}
<div className="space-y-8">
{/* 教程项1 */}
<Card className="overflow-hidden border-purple-200 dark:border-purple-900/50">
<CardHeader className="bg-purple-50 dark:bg-purple-900/20">
<CardTitle className="text-xl text-purple-700 dark:text-purple-400">1. Yggdrasil验证服务</CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-gray-600 dark:text-gray-400 mb-4">
Yggdrasil是Minecraft使用的身份验证服务协议使Yggdrasil协议的身份验证服务使
</p>
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-900/50">
<h4 className="font-semibold text-blue-700 dark:text-blue-400 mb-2"></h4>
<ul className="list-disc list-inside space-y-1 text-gray-600 dark:text-gray-400">
<li>使</li>
<li></li>
<li></li>
</ul>
</div>
</CardContent>
</Card>
{/* 教程项2 */}
<Card className="overflow-hidden border-purple-200 dark:border-purple-900/50">
<CardHeader className="bg-purple-50 dark:bg-purple-900/20">
<CardTitle className="text-xl text-purple-700 dark:text-purple-400">2. </CardTitle>
</CardHeader>
<CardContent className="p-6">
<h3 className="text-lg font-semibold text-gray-800 dark:text-white mb-4"></h3>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Minecraft客户端中使用HITWH.GAMES皮肤系统使Yggdrasil验证服务
</p>
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-900/50 mb-6">
<p className="text-gray-600 dark:text-gray-400">
使HITWH.GAMES网站上注册账号并创建角色
</p>
</div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white mb-4"></h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
HMCLPCL
</p>
{/* 拖放配置卡片 */}
<DragConfigCard
authUrl="https://skin.littlelan.cn/api/yggdrasil"
serviceName="HITWH.GAMES 皮肤系统"
/>
<h4 className="font-semibold text-gray-800 dark:text-white mb-2"></h4>
<ol className="list-decimal list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4 mb-6">
<li>HMCLPCL等</li>
<li>绿</li>
<li>"确定"</li>
<li>使HITWH.GAMES账号登录</li>
</ol>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">HMCL启动器配置</h3>
<ol className="list-decimal list-inside space-y-3 text-gray-600 dark:text-gray-400 ml-4 mb-8">
<li>HMCL启动器</li>
<li>"账户"</li>
<li>"添加认证服务器"</li>
<li>
"服务器地址"<br/>
<code className="bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded block mt-2">https://skin.littlelan.cn/api/yggdrasil</code>
</li>
<li>"下一步"HITWH.GAMES的用户名和密码</li>
<li>"登录"使HITWH.GAMES账号</li>
</ol>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">PCL启动器配置</h3>
<ol className="list-decimal list-inside space-y-3 text-gray-600 dark:text-gray-400 ml-4 mb-8">
<li>PCL启动器</li>
<li>"设置"</li>
<li>"登录设置"</li>
<li>"外置登录"</li>
<li>
"认证服务器"<br/>
<code className="bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded block mt-2">https://skin.littlelan.cn/api/yggdrasil</code>
</li>
<li>"保存"</li>
<li>HITWH.GAMES的用户名和密码进行登录</li>
</ol>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white mb-4"></h3>
<div className="space-y-4">
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 className="font-semibold text-gray-800 dark:text-white mb-2"></h4>
<p className="text-gray-600 dark:text-gray-400">
</p>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 className="font-semibold text-gray-800 dark:text-white mb-2"></h4>
<p className="text-gray-600 dark:text-gray-400">
</p>
</div>
</div>
</CardContent>
</Card>
{/* 教程项3 */}
<Card className="overflow-hidden border-purple-200 dark:border-purple-900/50">
<CardHeader className="bg-purple-50 dark:bg-purple-900/20">
<CardTitle className="text-xl text-purple-700 dark:text-purple-400">3. </CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-gray-600 dark:text-gray-400 mb-4">
Yggdrasil服务支持多账号管理
</p>
<div className="space-y-4">
<h4 className="font-semibold text-gray-800 dark:text-white"></h4>
<ol className="list-decimal list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4">
<li></li>
<li>"账户管理"</li>
<li>"创建子账户"</li>
<li></li>
<li></li>
</ol>
</div>
</CardContent>
</Card>
{/* 教程项4 */}
<Card className="overflow-hidden border-purple-200 dark:border-purple-900/50">
<CardHeader className="bg-purple-50 dark:bg-purple-900/20">
<CardTitle className="text-xl text-purple-700 dark:text-purple-400">4. </CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-4">
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 className="font-semibold text-gray-800 dark:text-white mb-2">Yggdrasil账号</h4>
<p className="text-gray-600 dark:text-gray-400">
Yggdrasil验证功能Yggdrasil集成文档
</p>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 className="font-semibold text-gray-800 dark:text-white mb-2">使Yggdrasil服务</h4>
<p className="text-gray-600 dark:text-gray-400">
HTTPS支持Yggdrasil服务器的连接
</p>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 className="font-semibold text-gray-800 dark:text-white mb-2"></h4>
<p className="text-gray-600 dark:text-gray-400">
退
</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 页面导航按钮 */}
<div className="flex justify-between items-center mt-12">
<Link href="/help/basic">
<Button variant="ghost" className="text-gray-600 dark:text-gray-400">
</Button>
</Link>
<Link href="/help/multilogin">
<Button className="bg-amber-600 hover:bg-amber-700 text-white">
MultiLogin教程
</Button>
</Link>
</div>
</div>
</main>
);
}

View File

@@ -3,6 +3,8 @@ import './globals.css';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import Navbar from '@/components/Navbar'; import Navbar from '@/components/Navbar';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/api/auth';
const inter = Inter({ subsets: ['latin'] }); const inter = Inter({ subsets: ['latin'] });
const grassIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAABCElEQVRYw+2W0Q6DIAxAZ3b/r7y7L0QNlKEFdLfEFyVQ+pVSCgBwB4DfGz4fQJxzGGOYc34d4DkAVVVVdQqgqioiIuccxhgAQFVVMMYg50wAY4w5gDEGKaWl0nPOoZQiAGPM7wGMMXDOwVqLtm2Rc0bXdTDGwDmHnPN/ADHGdF2HEAJijOj7HjFGpJTQ9z36vkeMESkl9H0PYwystYgxLgHGGHDOwTmHtm3hnEPbtmCMgXO+6DkA1lo45+C9R0oJwzAgpQRrLZxz8N4jpYRhGJBSgrUWzjlYa5cAzjkYY2CMQdM0YIyBMQbOOQEAwzCgrmtUVYW6rjEMA4ZhgDEGTdOAMQbOOQEA4ziiqio0TYO2bTGbYRiQc0bTNGiaBm3bYjY5Z4zjiKqqMAwD6rqGtRYpJYzjiJwzrLVomgZt22I2OWeM44i6rjEMA+q6RkoJ4zgi5wxrLZqmwWyGYUDOGXVdYxxH1HWNlBLGcUTOGdZaNE2D2QzDgJwz6rrGOI6o6xopJYzjiJwzrLVomgazGYYBOWfUdY1xHFHXNVJKGMfxfwB1XWO2OWeM44i6rjGOI+q6RkoJ4zgi5wxrLZqmwWyGYUDOGXVdYxxH1HWNlBLGcUTOGdZaNE2D2QzDgJwz6rrGOI6o6xopJYzj+AAQY4QQQgghhBBCCCGEkHv7AQm0VqDdH3i9AAAAAElFTkSuQmCC'; const grassIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAABCElEQVRYw+2W0Q6DIAxAZ3b/r7y7L0QNlKEFdLfEFyVQ+pVSCgBwB4DfGz4fQJxzGGOYc34d4DkAVVVVdQqgqioiIuccxhgAQFVVMMYg50wAY4w5gDEGKaWl0nPOoZQiAGPM7wGMMXDOwVqLtm2Rc0bXdTDGwDmHnPN/ADHGdF2HEAJijOj7HjFGpJTQ9z36vkeMESkl9H0PYwystYgxLgHGGHDOwTmHtm3hnEPbtmCMgXO+6DkA1lo45+C9R0oJwzAgpQRrLZxz8N4jpYRhGJBSgrUWzjlYa5cAzjkYY2CMQdM0YIyBMQbOOQEAwzCgrmtUVYW6rjEMA4ZhgDEGTdOAMQbOOQEA4ziiqio0TYO2bTGbYRiQc0bTNGiaBm3bYjY5Z4zjiKqqMAwD6rqGtRYpJYzjiJwzrLVomgZt22I2OWeM44i6rjEMA+q6RkoJ4zgi5wxrLZqmwWyGYUDOGXVdYxxH1HWNlBLGcUTOGdZaNE2D2QzDgJwz6rrGOI6o6xopJYzjiJwzrLVomgazGYYBOWfUdY1xHFHXNVJKGMfxfwB1XWO2OWeM44i6rjGOI+q6RkoJ4zgi5wxrLZqmwWyGYUDOGXVdYxxH1HWNlBLGcUTOGdZaNE2D2QzDgJwz6rrGOI6o6xopJYzj+AAQY4QQQgghhBBCCCGEkHv7AQm0VqDdH3i9AAAAAElFTkSuQmCC';
@@ -15,11 +17,14 @@ export const metadata: Metadata = {
}, },
}; };
export default function RootLayout({ export default async function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
// 在服务器端获取会话状态
const session = await getServerSession(authOptions);
return ( return (
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
@@ -28,9 +33,14 @@ export default function RootLayout({
rel="stylesheet" rel="stylesheet"
/> />
</head> </head>
<body className={`${inter.className} bg-gray-50 dark:bg-gray-900`}> <body className={`${inter.className} bg-gray-50 dark:bg-gray-900 min-h-screen flex flex-col`}>
<Navbar /> <Navbar session={session} />
<main className="container mx-auto px-4 py-8">{children}</main> <main className="flex-grow container mx-auto px-4 py-6 sm:py-8">{children}</main>
<footer className="bg-gray-800 text-white py-4 text-center text-sm">
<div className="container mx-auto px-4">
© 2024 - Minecraft玩家打造的皮肤分享平台
</div>
</footer>
</body> </body>
</html> </html>
); );

View File

@@ -1,54 +1,153 @@
// src/app/page.tsx // src/app/page.tsx
//这个是介绍首页页面
'use client';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { useEffect, useRef, useState } from 'react';
import Canvas2DSkinPreview from '@/components/skins/Canvas2DSkinPreview';
import ServerInfoBubble, { ServerInfoBubbleStyles } from '@/components/ServerInfoBubble';
// 渲染样式
function BubbleStyles() {
return <style>{ServerInfoBubbleStyles}</style>;
}
// 功能卡片组件
function FeatureCard({ title, description, icon }: { title: string; description: string; icon: string }) {
return (
<Card className="overflow-hidden transition-all duration-300 hover:shadow-xl bg-white dark:bg-gray-800 border-0 shadow-lg h-full flex flex-col">
<CardHeader className="bg-gradient-to-r from-emerald-50 to-teal-50 dark:from-emerald-900/20 dark:to-teal-900/20 border-b border-emerald-100 dark:border-emerald-900/30 pb-5">
<div className="text-4xl mb-3">{icon}</div>
<CardTitle className="text-xl font-bold text-gray-800 dark:text-white">{title}</CardTitle>
</CardHeader>
<CardContent className="pt-6 pb-6 px-6 flex-grow">
<p className="text-gray-600 dark:text-gray-400">{description}</p>
</CardContent>
</Card>
);
}
export default function HomePage() { export default function HomePage() {
return ( const titleRef = useRef<HTMLHeadingElement>(null);
<div className="min-h-screen bg-gradient-to-br from-green-50 to-cyan-100 dark:from-gray-900 dark:to-gray-800"> const descRef = useRef<HTMLParagraphElement>(null);
{/* 导航栏 */}
<nav className="bg-white dark:bg-gray-800 shadow-md py-4 px-6 flex justify-between items-center"> // 打字机效果状态
<div className="flex items-center space-x-2"> const [displayText, setDisplayText] = useState('');
<div className="bg-gray-200 border-2 border-dashed rounded-xl w-10 h-10" /> const [displayHighlightText, setDisplayHighlightText] = useState('');
<span className="text-xl font-bold text-green-600 dark:text-green-400">HITWH皮肤库</span> const [cursorVisible, setCursorVisible] = useState(true);
</div> const fullText = '创建、分享和管理你的';
<div className="flex space-x-4"> const highlightText = 'Minecraft 皮肤';
<Button asChild variant="outline" className="border-green-600 text-green-600 hover:bg-green-50"> const [isTypingMainText, setIsTypingMainText] = useState(true);
<Link href="/login"></Link> const [typingComplete, setTypingComplete] = useState(false);
</Button>
<Button asChild className="bg-green-600 hover:bg-green-700">
<Link href="/register"></Link>
</Button>
</div>
</nav>
{/* 区域 */} useEffect(() => {
<section className="container mx-auto px-4 py-16 flex flex-col md:flex-row items-center justify-between"> // 打字机效果
<div className="md:w-1/2 mb-12 md:mb-0"> let mainTextIndex = 0;
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 dark:text-white mb-6"> let highlightTextIndex = 0;
<span className="text-green-600 dark:text-green-400"> Minecraft </span> const typeInterval = setInterval(() => {
</h1> if (isTypingMainText) {
<p className="text-lg text-gray-600 dark:text-gray-300 mb-8 max-w-lg"> if (mainTextIndex < fullText.length) {
3D预览中查看效果HITWH的大家分享你的创作 setDisplayText(fullText.slice(0, mainTextIndex + 1));
</p> mainTextIndex++;
<div className="flex flex-col sm:flex-row gap-4"> } else {
<Button asChild size="lg" className="text-lg bg-green-600 hover:bg-green-700 py-6"> setIsTypingMainText(false);
}
} else {
if (highlightTextIndex < highlightText.length) {
setDisplayHighlightText(highlightText.slice(0, highlightTextIndex + 1));
highlightTextIndex++;
} else {
clearInterval(typeInterval);
setTypingComplete(true);
}
}
}, 100); // 每个字符100ms的打字速度
// 光标闪烁效果
const cursorInterval = setInterval(() => {
setCursorVisible(prev => !prev);
}, 530); // 光标闪烁频率
// 清理函数
return () => {
clearInterval(typeInterval);
clearInterval(cursorInterval);
};
}, [isTypingMainText]);
useEffect(() => {
// 确保标题和描述文本始终可见
if (titleRef.current) {
titleRef.current.style.opacity = '1';
titleRef.current.style.transform = 'translateY(0)';
}
if (descRef.current) {
descRef.current.style.opacity = '1';
descRef.current.style.transform = 'translateY(0)';
}
}, []); // 空依赖数组,确保只在组件加载时执行一次
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800 relative overflow-hidden">
{/* 服务器信息气泡组件 */}
<ServerInfoBubble zIndex={1000} dragDistanceLimit={300} />
<BubbleStyles />
{/* 背景装饰元素 - 渐变模糊效果 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-20 -right-20 w-80 h-80 bg-emerald-400/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-20 -left-20 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl"></div>
<div className="absolute top-1/3 left-1/4 w-64 h-64 bg-emerald-300/10 rounded-full blur-3xl"></div>
</div>
{/* 英雄区域 */}
<section className="container mx-auto px-4 py-24 flex flex-col md:flex-row items-center justify-between gap-12 relative z-10">
<div className="md:w-1/2 mb-12 md:mb-0 space-y-8">
<div className="space-y-4">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-800 dark:text-white tracking-tight leading-tight">
{displayText}
<span className="text-emerald-600 dark:text-emerald-400">
{displayHighlightText}
</span>
<span className={`inline-block w-2 h-8 ml-1 bg-gray-800 dark:bg-white transition-opacity ${cursorVisible ? 'opacity-100' : 'opacity-0'}`}>
</span>
</h1>
<p className="text-lg md:text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-lg leading-relaxed">
HITWH的大家分享你的创作
</p>
</div>
<div className="flex flex-col sm:flex-row gap-5">
<Button asChild size="lg" className="text-lg bg-emerald-600 hover:bg-emerald-700 py-6 px-8 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
<Link href="/skins/upload"></Link> <Link href="/skins/upload"></Link>
</Button> </Button>
<Button asChild variant="outline" size="lg" className="text-lg border-green-600 text-green-600 hover:bg-green-50 py-6"> <Button asChild variant="outline" size="lg" className="text-lg border-emerald-600 text-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-900/10 py-6 px-8 rounded-xl shadow-md hover:shadow-lg transition-all duration-300">
<Link href="/dashboard"></Link> <Link href="/user-home"></Link>
</Button> </Button>
</div> </div>
</div> </div>
<div className="md:w-1/2 flex justify-center"> <div className="md:w-1/2 flex justify-center">
<div className="relative w-full max-w-md"> <div className="relative w-full max-w-md">
<div className="absolute inset-0 bg-green-500 rounded-2xl transform rotate-6"></div> {/* 装饰性背景元素 */}
<div className="relative bg-gray-200 border-2 border-dashed rounded-xl w-full h-96 flex items-center justify-center"> <div className="absolute -inset-4 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-3xl transform rotate-3 blur-xl opacity-20"></div>
<div className="text-center p-6"> <div className="absolute -right-6 -bottom-6 w-32 h-32 bg-emerald-400 rounded-full blur-2xl opacity-20"></div>
<h3 className="text-xl font-bold mb-4"></h3>
<p className="text-gray-600">3D皮肤预览</p> <div className="relative bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm rounded-2xl w-full h-96 flex items-center justify-center border border-emerald-200 dark:border-emerald-900/30 shadow-xl overflow-hidden">
{/* 背景装饰 */}
<div className="absolute top-0 left-0 right-0 h-1/3 bg-gradient-to-b from-emerald-50 to-teal-50 dark:from-emerald-900/10 dark:to-teal-900/10"></div>
<div className="absolute -bottom-16 -right-16 w-32 h-32 bg-emerald-200 dark:bg-emerald-900/20 rounded-full blur-xl"></div>
<div className="text-center p-8 relative z-10">
<div className="flex justify-center mb-6">
<Canvas2DSkinPreview
skinUrl="/test-skin.png"
size={192}
className="border-4 border-emerald-100 dark:border-emerald-800/50 rounded-lg shadow-lg"
/>
</div>
<h3 className="text-2xl font-bold mb-4 text-gray-800 dark:text-white"></h3>
<p className="text-gray-600 dark:text-gray-400 max-w-xs mx-auto"></p>
</div> </div>
</div> </div>
</div> </div>
@@ -56,16 +155,34 @@ export default function HomePage() {
</section> </section>
{/* 功能特性区域 */} {/* 功能特性区域 */}
<section className="py-16 bg-white dark:bg-gray-800"> <section className="py-20 bg-white dark:bg-gray-800">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<h2 className="text-3xl font-bold text-center text-gray-800 dark:text-white mb-16"> {/* 强调区域 */}
<div className="mb-20 rounded-2xl overflow-hidden max-w-4xl mx-auto shadow-xl">
<div className="bg-gradient-to-r from-emerald-600 via-teal-500 to-cyan-500 py-16 px-8 text-center backdrop-blur-sm">
<h3
ref={titleRef}
className="text-3xl md:text-4xl font-bold text-white mb-6 transition-all duration-1000"
>
</h3>
<p
ref={descRef}
className="text-xl text-white/90 max-w-3xl mx-auto transition-all duration-1000"
>
Minecraft世界中脱颖而出
</p>
</div>
</div>
<h2 className="text-3xl md:text-4xl font-bold text-center text-gray-800 dark:text-white mb-16 tracking-tight">
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
<FeatureCard <FeatureCard
title="3D实时预览" title="皮肤预览"
description="上传后立即在3D模型中查看皮肤效果,支持360度旋转查看" description="上传后立即查看皮肤效果,清晰展示头部正脸细节"
icon="👁️" icon="👁️"
/> />
<FeatureCard <FeatureCard
@@ -75,7 +192,7 @@ export default function HomePage() {
/> />
<FeatureCard <FeatureCard
title="社区分享" title="社区分享"
description="社区认同和安全保障(大概?" description="与HITWH的大家分享你的创作获得社区认同和反馈"
icon="🌍" icon="🌍"
/> />
</div> </div>
@@ -83,19 +200,23 @@ export default function HomePage() {
</section> </section>
{/* 行动号召区域 */} {/* 行动号召区域 */}
<section className="py-20 bg-gradient-to-r from-green-500 to-cyan-500 dark:from-green-700 dark:to-cyan-700"> <section className="py-24 bg-gradient-to-r from-emerald-600 to-teal-500 dark:from-emerald-700 dark:to-teal-700 relative overflow-hidden">
<div className="container mx-auto px-4 text-center"> {/* 装饰性背景元素 */}
<h2 className="text-3xl md:text-4xl font-bold text-white mb-6"> <div className="absolute top-1/4 left-1/4 w-64 h-64 bg-white/10 rounded-full blur-3xl"></div>
HITWH300众 <div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
<div className="container mx-auto px-4 text-center relative z-10">
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6 tracking-tight leading-tight">
HITWH皮肤社区
</h2> </h2>
<p className="text-xl text-green-100 max-w-2xl mx-auto mb-10"> <p className="text-xl text-white/90 max-w-2xl mx-auto mb-12 leading-relaxed">
HITWH的大家一起探索无限可能
</p> </p>
<div className="flex flex-col sm:flex-row justify-center gap-4"> <div className="flex flex-col sm:flex-row justify-center gap-5">
<Button asChild size="lg" className="text-lg bg-white text-green-600 hover:bg-green-50 py-6"> <Button asChild size="lg" className="text-lg bg-white text-emerald-600 hover:bg-emerald-50 py-6 px-8 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
<Link href="/register"></Link> <Link href="/register"></Link>
</Button> </Button>
<Button asChild variant="outline" size="lg" className="text-lg border-white text-white hover:bg-green-600 py-6"> <Button asChild variant="outline" size="lg" className="text-lg border-white text-white hover:bg-white/10 py-6 px-8 rounded-xl shadow-md hover:shadow-lg transition-all duration-300">
<Link href="/login"></Link> <Link href="/login"></Link>
</Button> </Button>
</div> </div>
@@ -103,72 +224,54 @@ export default function HomePage() {
</section> </section>
{/* 页脚 */} {/* 页脚 */}
<footer className="bg-gray-800 text-gray-300 py-10"> <footer className="bg-gray-900/95 backdrop-blur-md text-gray-300 py-16 relative z-10">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center"> <div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-6 md:mb-0"> <div className="mb-10 md:mb-0 text-center md:text-left">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-3 justify-center md:justify-start">
<div className="bg-gray-200 border-2 border-dashed rounded-xl w-8 h-8" /> <Image src="/images/mc-favicon.ico" alt="Logo" width={40} height={40} className="rounded-xl w-10 h-10" />
<span className="text-xl font-bold text-green-400">HITWH皮肤库</span> <span className="text-2xl font-bold text-emerald-400">HITWH皮肤库</span>
</div> </div>
<p className="mt-2 text-sm"> Minecraft </p> <p className="mt-3 text-sm text-gray-400 max-w-xs mx-auto md:mx-0"> Minecraft </p>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-8"> <div className="grid grid-cols-2 md:grid-cols-3 gap-8">
<div> <div>
<h3 className="text-green-400 font-semibold mb-3"></h3> <h3 className="text-emerald-400 font-semibold mb-4 text-lg"></h3>
<ul className="space-y-2"> <ul className="space-y-3">
<li><Link href="/" className="hover:text-white"></Link></li> <li><Link href="/" className="hover:text-white transition-colors"></Link></li>
<li><Link href="/dashboard" className="hover:text-white"></Link></li> <li><Link href="/dashboard" className="hover:text-white transition-colors"></Link></li>
<li><Link href="/skins/upload" className="hover:text-white"></Link></li> <li><Link href="/skins/upload" className="hover:text-white transition-colors"></Link></li>
<li><Link href="/character-center" className="hover:text-white transition-colors"></Link></li>
</ul> </ul>
</div> </div>
<div> <div>
<h3 className="text-green-400 font-semibold mb-3"></h3> <h3 className="text-emerald-400 font-semibold mb-4 text-lg"></h3>
<ul className="space-y-2"> <ul className="space-y-3">
<li><Link href="/login" className="hover:text-white"></Link></li> <li><Link href="/login" className="hover:text-white transition-colors"></Link></li>
<li><Link href="/register" className="hover:text-white"></Link></li> <li><Link href="/register" className="hover:text-white transition-colors"></Link></li>
<li><Link href="/dashboard" className="hover:text-white"></Link></li> <li><Link href="/dashboard" className="hover:text-white transition-colors"></Link></li>
<li><Link href="/user-home" className="hover:text-white transition-colors"></Link></li>
</ul> </ul>
</div> </div>
<div> <div>
<h3 className="text-green-400 font-semibold mb-3"></h3> <h3 className="text-emerald-400 font-semibold mb-4 text-lg"></h3>
<ul className="space-y-2"> <ul className="space-y-3">
<li><Link href="/help" className="hover:text-white"></Link></li> <li><Link href="/help" className="hover:text-white transition-colors"></Link></li>
<li><Link href="/contact" className="hover:text-white"></Link></li> <li><Link href="/contact" className="hover:text-white transition-colors"></Link></li>
<li><Link href="/terms" className="hover:text-white"></Link></li> <li><Link href="/terms" className="hover:text-white transition-colors"></Link></li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div className="border-t border-gray-700 mt-10 pt-6 text-center text-sm"> <div className="border-t border-gray-800 mt-12 pt-8 text-center text-sm text-gray-400">
<p>© {new Date().getFullYear()} HITWHGAMES. .</p> <p>© {new Date().getFullYear()} HITWHGAMES. .</p>
</div> </div>
</div> </div>
</footer> </footer>
</div> </div>
); );
}
// 功能卡片组件
function FeatureCard({ title, description, icon }: { title: string; description: string; icon: string }) {
return (
<Card className="h-full transition-transform hover:scale-[1.02] hover:shadow-lg">
<CardHeader>
<div className="text-4xl mb-4">{icon}</div>
<CardTitle className="text-xl">{title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 dark:text-gray-300">{description}</p>
</CardContent>
<CardFooter>
<Button variant="link" className="text-green-600 dark:text-green-400 p-0">
</Button>
</CardFooter>
</Card>
);
} }

View File

@@ -1,6 +1,8 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import SkinViewer3D from '@/components/skins/SkinViewer3D';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import Link from 'next/link';
import Canvas2DSkinPreview from '@/components/skins/Canvas2DSkinPreview';
interface SkinDetailProps { interface SkinDetailProps {
params: { id: string }; params: { id: string };
@@ -14,33 +16,111 @@ export default function SkinDetail({ params }: SkinDetailProps) {
description: '这是你的Minecraft角色皮肤', description: '这是你的Minecraft角色皮肤',
createdAt: '2023-06-01', createdAt: '2023-06-01',
downloadUrl: `/api/skins/${params.id}/download`, downloadUrl: `/api/skins/${params.id}/download`,
author: '创作者名称',
tags: ['游戏', '角色', '自定义']
}; };
if (!skinData) return notFound(); if (!skinData) return notFound();
return ( return (
<div className="max-w-4xl mx-auto"> <div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800 relative overflow-hidden py-12">
<h1 className="text-3xl font-bold mb-6">{skinData.name}</h1> {/* 背景装饰元素 - 渐变模糊效果 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="absolute -top-20 -right-20 w-80 h-80 bg-emerald-400/20 rounded-full blur-3xl"></div>
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-4"> <div className="absolute -bottom-20 -left-20 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl"></div>
<SkinViewer3D skinUrl={skinData.downloadUrl} /> <div className="absolute top-1/3 left-1/4 w-64 h-64 bg-emerald-300/10 rounded-full blur-3xl"></div>
</div> </div>
<div> <div className="container mx-auto px-4 max-w-6xl relative z-10">
<p className="mb-4">{skinData.description}</p> <div className="mb-8 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<p className="text-gray-500 mb-6">: {skinData.createdAt}</p> <div>
<h1 className="text-3xl font-bold text-gray-800 dark:text-white mb-2 tracking-tight"></h1>
<div className="flex gap-3"> <p className="text-gray-600 dark:text-gray-400">Minecraft皮肤</p>
<Button asChild>
<a href={skinData.downloadUrl} download></a>
</Button>
<Button variant="outline">
</Button>
</div> </div>
<Button asChild variant="ghost" className="hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-300">
<Link href="/skins"></Link>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 皮肤预览卡片 */}
<Card className="border border-emerald-200 dark:border-emerald-900/50 shadow-xl rounded-2xl overflow-hidden bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardHeader className="bg-emerald-50 dark:bg-gray-800/80 border-b border-emerald-100 dark:border-gray-700 pb-6">
<CardTitle className="text-xl text-gray-800 dark:text-white"></CardTitle>
</CardHeader>
<CardContent className="pt-6">
<div className="bg-gray-50 dark:bg-gray-900 rounded-xl p-6 aspect-square flex items-center justify-center relative overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<Canvas2DSkinPreview
skinUrl="/test-skin2.png"
size={256}
className="max-w-full max-h-full"
/>
</div>
</CardContent>
</Card>
{/* 皮肤信息卡片 */}
<Card className="border border-emerald-200 dark:border-emerald-900/50 shadow-xl rounded-2xl overflow-hidden bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardHeader className="bg-emerald-50 dark:bg-gray-800/80 border-b border-emerald-100 dark:border-gray-700 pb-6">
<CardTitle className="text-xl text-gray-800 dark:text-white">{skinData.name}</CardTitle>
</CardHeader>
<CardContent className="pt-6 space-y-6">
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400"></h3>
<p className="text-gray-700 dark:text-gray-300 leading-relaxed">{skinData.description}</p>
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400"></h3>
<p className="text-gray-700 dark:text-gray-300">{skinData.author}</p>
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400"></h3>
<p className="text-gray-700 dark:text-gray-300">{skinData.createdAt}</p>
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400"></h3>
<div className="flex flex-wrap gap-2">
{skinData.tags.map((tag, index) => (
<span key={index} className="px-3 py-1 bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-400 rounded-full text-sm">
{tag}
</span>
))}
</div>
</div>
</CardContent>
<CardFooter className="border-t border-emerald-100 dark:border-gray-700 py-6 px-6 bg-emerald-50/50 dark:bg-gray-800/80">
<div className="w-full flex gap-4">
<Button
asChild
className="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white transition-all duration-300 transform hover:-translate-y-1 shadow-md hover:shadow-lg"
>
<a href={skinData.downloadUrl} download className="w-full"></a>
</Button>
<Button
variant="outline"
className="flex-1 border-emerald-600 text-emerald-600 hover:bg-emerald-50 dark:border-emerald-400 dark:text-emerald-400 dark:hover:bg-emerald-900/20 transition-all duration-300"
>
</Button>
</div>
</CardFooter>
</Card>
</div> </div>
</div> </div>
{/* 页脚 */}
<footer className="bg-gray-900/80 backdrop-blur-md text-gray-300 py-8 mt-16 border-t border-gray-800">
<div className="container mx-auto px-4 text-center relative z-10">
<p className="mb-2"> - Minecraft皮肤分享平台</p>
<p className="text-sm text-gray-400">© {new Date().getFullYear()} </p>
</div>
</footer>
</div> </div>
); );
} }

177
src/app/skins/page.tsx Normal file
View File

@@ -0,0 +1,177 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardFooter } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import Link from 'next/link';
import Canvas2DSkinPreview from '@/components/skins/Canvas2DSkinPreview';
export default function SkinsPage() {
// 模拟皮肤数据添加type字段区分皮肤类型steve、alex或cape
const allSkins = [
{ id: '1', name: '经典Steve皮肤', author: 'Mojang', createdAt: '2023-05-01', tags: ['经典', '默认'], imageUrl: '/test-skin.png', type: 'steve' },
{ id: '2', name: '史诗战士Steve', author: '玩家123', createdAt: '2023-06-15', tags: ['战士', '冒险'], imageUrl: '/test-skin.png', type: 'steve' },
{ id: '3', name: '魔法师学徒Steve', author: '创意大师', createdAt: '2023-07-20', tags: ['魔法', '幻想'], imageUrl: '/test-skin.png', type: 'steve' },
{ id: '4', name: '经典Alex皮肤', author: 'Mojang', createdAt: '2023-08-10', tags: ['经典', '默认'], imageUrl: '/test-skin2.png', type: 'alex' },
{ id: '5', name: '太空探险Alex', author: '星际旅行者', createdAt: '2023-09-05', tags: ['科技', '太空'], imageUrl: '/test-skin2.png', type: 'alex' },
{ id: '6', name: '像素艺术家Alex', author: '方块创作者', createdAt: '2023-10-12', tags: ['艺术', '现代'], imageUrl: '/test-skin2.png', type: 'alex' },
{ id: '7', name: '英雄披风', author: '披风大师', createdAt: '2023-11-01', tags: ['英雄', '经典'], imageUrl: '/test-skin3.png', type: 'cape' },
{ id: '8', name: '龙纹披风', author: '东方设计师', createdAt: '2023-12-15', tags: ['龙', '东方'], imageUrl: '/test-skin3.png', type: 'cape' },
];
// 过滤和搜索状态
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
// 过滤皮肤 - 按类型steve/alex/cape分类
const filteredSkins = allSkins.filter(skin => {
const matchesSearch = skin.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
skin.author.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = selectedCategory === 'all' ||
(selectedCategory === 'Steve' && skin.type === 'steve') ||
(selectedCategory === 'Alex' && skin.type === 'alex') ||
(selectedCategory === '披风' && skin.type === 'cape');
return matchesSearch && matchesCategory;
});
return (
<div className="min-h-screen py-12 relative">
{/* 马赛克砖背景样式 */}
<div className="absolute inset-0 z-0 bg-[radial-gradient(#e6f7ef_15%,transparent_16%)] bg-[length:24px_24px] dark:bg-[radial-gradient(rgba(16,185,129,0.1)_15%,transparent_16%)] dark:bg-[length:28px_28px]"></div>
<div className="container mx-auto px-4 max-w-6xl relative z-10">
<div className="mb-8 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-3xl font-bold text-gray-800 dark:text-white mb-2 tracking-tight"></h1>
<p className="text-gray-600 dark:text-gray-400">Minecraft皮肤</p>
</div>
<Button
asChild
className="bg-emerald-600 hover:bg-emerald-700 text-white transition-all duration-300 transform hover:-translate-y-1 shadow-md hover:shadow-lg"
>
<Link href="/skins/upload"></Link>
</Button>
</div>
{/* 搜索和筛选区 */}
<Card className="mb-8 border border-emerald-200 dark:border-emerald-900/50 shadow-lg rounded-2xl overflow-hidden bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm">
<CardContent className="pt-6 px-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="relative col-span-2">
<Input
placeholder="搜索皮肤名称或作者..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 border-emerald-200 dark:border-gray-700 rounded-xl focus-visible:ring-emerald-500 dark:focus-visible:ring-emerald-400"
/>
<svg className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<div className="grid grid-cols-3 gap-2 col-span-1">
<Button
variant={selectedCategory === 'all' ? 'default' : 'secondary'}
onClick={() => setSelectedCategory('all')}
className="text-sm"
>
</Button>
<Button
variant={selectedCategory === 'Steve' ? 'default' : 'secondary'}
onClick={() => setSelectedCategory('Steve')}
className="text-sm"
>
Steve
</Button>
<Button
variant={selectedCategory === 'Alex' ? 'default' : 'secondary'}
onClick={() => setSelectedCategory('Alex')}
className="text-sm"
>
Alex
</Button>
<Button
variant={selectedCategory === '披风' ? 'default' : 'secondary'}
onClick={() => setSelectedCategory('披风')}
className="text-sm"
>
</Button>
</div>
</div>
</CardContent>
</Card>
{/* 皮肤列表 */}
{filteredSkins.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{filteredSkins.map((skin) => (
<Card
key={skin.id}
className={`border ${skin.type === 'steve' ? 'border-blue-200 dark:border-blue-900/50' : skin.type === 'alex' ? 'border-pink-200 dark:border-pink-900/50' : 'border-purple-200 dark:border-purple-900/50'} shadow-lg rounded-2xl overflow-hidden bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm transition-all duration-300 hover:shadow-xl transform hover:-translate-y-1 relative`}
>
<Link href={`/skins/${skin.id}`} className="block">
<div className="aspect-square bg-gray-50 dark:bg-gray-900 flex items-center justify-center p-4 relative overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<Canvas2DSkinPreview
skinUrl={skin.imageUrl}
size={128}
className="max-w-full max-h-full relative z-10 transition-transform duration-500 group-hover:scale-110"
/>
</div>
<CardContent className="pt-6 pb-4 px-6">
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="font-bold text-lg text-gray-800 dark:text-white group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors">{skin.name}</h3>
<span className={`text-xs px-2 py-1 rounded-full ${skin.type === 'steve' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' : skin.type === 'alex' ? 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-400' : 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'}`}>
{skin.type === 'steve' ? 'Steve' : skin.type === 'alex' ? 'Alex' : '披风'}
</span>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">: {skin.author}</p>
<div className="flex flex-wrap gap-2 mt-2">
{skin.tags.map((tag, index) => (
<div
key={index}
className="inline-block px-2 py-1 text-xs bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-400 rounded-full"
>
{tag}
</div>
))}
</div>
</div>
</CardContent>
<CardFooter className="px-6 py-4 border-t border-emerald-100 dark:border-gray-700 bg-emerald-50/50 dark:bg-gray-800/80">
<div className="w-full flex justify-between items-center">
<span className="text-xs text-gray-500 dark:text-gray-400"> {skin.createdAt}</span>
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium"> </span>
</div>
</CardFooter>
</Link>
</Card>
))}
</div>
) : (
<Card className="border border-emerald-200 dark:border-emerald-900/50 shadow-lg rounded-2xl overflow-hidden bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm text-center py-12">
<CardContent className="space-y-4">
<div className="text-6xl mb-4">🔍</div>
<h3 className="text-xl font-bold text-gray-800 dark:text-white"></h3>
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
使
</p>
<Button
className="mt-4 bg-emerald-600 hover:bg-emerald-700 text-white transition-all duration-300 transform hover:-translate-y-1 shadow-md hover:shadow-lg"
onClick={() => {
setSearchTerm('');
setSelectedCategory('all');
}}
>
</Button>
</CardContent>
</Card>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,151 @@
'use client';
import { useEffect, useRef, useState } from 'react';
// 全局样式以确保像素化渲染
const styles = `
.image-rendering-pixelated {
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
width: 300px;
height: 300px;
}
`;
// 创建一个页面展示Minecraft皮肤头部的六个面
const MinecraftSkinPreview: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [face, setFace] = useState<string>('front'); // 默认为正面
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 设置canvas尺寸
const canvasSize = 300;
canvas.width = canvasSize;
canvas.height = canvasSize;
// 设置背景
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, canvasSize, canvasSize);
// 创建图像对象加载皮肤
const skinImage = new Image();
skinImage.src = '/test-skin.png';
skinImage.crossOrigin = 'anonymous'; // 确保可以跨域访问
skinImage.onload = () => {
// 皮肤尺寸固定为64x64
const skinWidth = 64;
const skinHeight = 64;
// 确保使用64x64的坐标系统
let sourceX = 0;
let sourceY = 0;
const sourceSize = 8; // 每个面的大小是8x8像素
// 根据选择的面设置源坐标 - 严格按照用户提供的UV映射表
switch (face) {
case 'front':
sourceX = 8; // X: 8-16
sourceY = 8; // Y: 8-16
break;
case 'back':
sourceX = 16; // X: 16-24
sourceY = 8; // Y: 8-16
break;
case 'top':
sourceX = 8; // X: 8-16
sourceY = 0; // Y: 0-8
break;
case 'bottom':
sourceX = 0; // X: 0-8
sourceY = 16; // Y: 16-24
break;
case 'right':
sourceX = 0; // X: 0-8
sourceY = 8; // Y: 8-16
break;
case 'left':
sourceX = 24; // X: 24-32
sourceY = 8; // Y: 8-16
break;
default:
sourceX = 8;
sourceY = 8;
}
// 计算缩放比例将8x8像素放大到canvas的大小
const scale = canvasSize / sourceSize;
// 设置图像渲染质量为像素化
ctx.imageSmoothingEnabled = false;
ctx.imageSmoothingQuality = 'low';
// 清除之前的内容
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, canvasSize, canvasSize);
// 直接绘制皮肤的头部部分
ctx.drawImage(
skinImage,
sourceX, sourceY, sourceSize, sourceSize, // 源区域
0, 0, canvasSize, canvasSize // 目标区域
);
};
// 错误处理
skinImage.onerror = () => {
console.error('无法加载皮肤文件');
ctx.fillStyle = '#ff0000';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('无法加载皮肤文件', canvasSize / 2, canvasSize / 2);
};
}, [face]);
return (
<>
<style>{styles}</style>
<div className="p-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-6">Minecraft </h1>
<div className="mb-6">
<label className="block mb-2 font-medium"></label>
<div className="flex flex-wrap gap-2">
{['front', 'back', 'top', 'bottom', 'right', 'left'].map((faceType) => (
<button
key={faceType}
onClick={() => setFace(faceType)}
className={`px-4 py-2 rounded-md transition-colors ${face === faceType ? 'bg-blue-600 text-white' : 'bg-gray-200 hover:bg-gray-300'}`}
>
{faceType === 'front' && '正面'}
{faceType === 'back' && '背面'}
{faceType === 'top' && '顶部'}
{faceType === 'bottom' && '底部'}
{faceType === 'right' && '右侧'}
{faceType === 'left' && '左侧'}
</button>
))}
</div>
</div>
<div className="flex justify-center">
<div className="bg-white p-4 rounded-lg shadow-md border border-gray-200">
<canvas
ref={canvasRef}
className="image-rendering-pixelated" // 确保浏览器也使用像素化渲染
/>
</div>
</div>
</div>
</>
);
};
export default MinecraftSkinPreview;

View File

@@ -1,12 +1,18 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import SkinUploader from '@/components/skins/SkinUploader';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { UploadIcon } from 'lucide-react';
import Link from 'next/link';
import Canvas2DSkinPreview from '@/components/skins/Canvas2DSkinPreview';
export default function SkinUploadPage() { export default function SkinUploadPage() {
const [skinFile, setSkinFile] = useState<File | null>(null); const [skinFile, setSkinFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [skinFileUrl, setSkinFileUrl] = useState<string>('');
const [skinName, setSkinName] = useState<string>('');
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
@@ -18,6 +24,19 @@ export default function SkinUploadPage() {
} }
}); });
// 当文件改变时创建预览URL
useEffect(() => {
if (skinFile) {
const objectUrl = URL.createObjectURL(skinFile);
setSkinFileUrl(objectUrl);
// 组件卸载时清理ObjectURL
return () => URL.revokeObjectURL(objectUrl);
} else {
setSkinFileUrl('');
}
}, [skinFile]);
const handleUpload = async () => { const handleUpload = async () => {
if (!skinFile) return; if (!skinFile) return;
@@ -38,36 +57,90 @@ export default function SkinUploadPage() {
}; };
return ( return (
<div className="max-w-2xl mx-auto"> <div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800 relative overflow-hidden py-12">
<h1 className="text-3xl font-bold mb-6"></h1> {/* 背景装饰元素 - 渐变模糊效果 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div <div className="absolute -top-20 -right-20 w-80 h-80 bg-emerald-400/20 rounded-full blur-3xl"></div>
{...getRootProps()} <div className="absolute -bottom-20 -left-20 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl"></div>
className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer mb-6" <div className="absolute top-1/3 left-1/4 w-64 h-64 bg-emerald-300/10 rounded-full blur-3xl"></div>
>
<input {...getInputProps()} />
<p className="text-gray-500">PNG格式的皮肤文件到这里</p>
<p className="text-sm text-gray-400 mt-2">64x32像素</p>
</div> </div>
{skinFile && ( <div className="container mx-auto px-4 max-w-2xl relative z-10">
<div className="mb-6"> <div className="mb-8 flex flex-col sm:flex-row justify-between items-center gap-4">
<h3 className="text-lg font-medium mb-3"></h3> <div>
<SkinUploader file={skinFile} /> <h1 className="text-3xl font-bold text-gray-800 dark:text-white mb-2 tracking-tight"></h1>
<div className="mt-3"> <p className="text-gray-600 dark:text-gray-400"></p>
<p>: {skinFile.name}</p>
<p>: {(skinFile.size / 1024).toFixed(2)} KB</p>
</div> </div>
<Button asChild variant="ghost" className="hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-300">
<Link href="/skins"></Link>
</Button>
</div> </div>
)}
<Card className="border border-emerald-200 dark:border-emerald-900/50 shadow-xl rounded-2xl overflow-hidden bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardHeader className="bg-emerald-50/50 dark:bg-gray-800/80 border-b border-emerald-100 dark:border-gray-700">
<CardTitle className="text-2xl text-gray-800 dark:text-white"></CardTitle>
<CardDescription className="text-gray-600 dark:text-gray-400">PNG格式的皮肤文件并查看预览</CardDescription>
</CardHeader>
<CardContent className="pt-6">
<div className="space-y-6">
<div className="space-y-3">
<Label className="text-gray-700 dark:text-gray-300 font-medium"></Label>
<div
{...getRootProps()}
className="border-2 border-dashed border-emerald-200 dark:border-emerald-900/30 rounded-xl p-8 text-center cursor-pointer hover:border-emerald-400 dark:hover:border-emerald-600/50 transition-all duration-300 bg-emerald-50/30 dark:bg-gray-700/30 group"
>
<input {...getInputProps()} />
<div className="space-y-3">
<UploadIcon className="mx-auto h-10 w-10 text-emerald-500 dark:text-emerald-400 transition-transform duration-300 group-hover:scale-110" />
<div className="space-y-2">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">PNG格式的皮肤文件到这里</p>
<p className="text-xs text-gray-500 dark:text-gray-400">64x32像素</p>
</div>
</div>
</div>
</div>
<Button <div className="space-y-4 p-4 border rounded-xl bg-white dark:bg-gray-700/50 border-emerald-100 dark:border-gray-700">
onClick={handleUpload} <h3 className="text-lg font-medium text-gray-700 dark:text-gray-300"></h3>
disabled={!skinFile || isUploading} <div className="flex justify-center">
className="w-full" <div className="w-full max-w-md aspect-square bg-gray-50 dark:bg-gray-900 rounded-lg shadow-md flex items-center justify-center">
> <Canvas2DSkinPreview
{isUploading ? '上传中...' : '上传皮肤'} skinUrl={skinFileUrl || "/test-skin.png"}
</Button> size={256}
className="max-w-full max-h-full"
/>
</div>
</div>
{skinFile && (
<div className="mt-3 space-y-1">
<p className="text-sm text-gray-600 dark:text-gray-300">: {skinFile.name}</p>
<p className="text-sm text-gray-600 dark:text-gray-300">: {(skinFile.size / 1024).toFixed(2)} KB</p>
</div>
)}
</div>
</div>
</CardContent>
<CardFooter className="bg-emerald-50/30 dark:bg-gray-800/50 border-t border-emerald-100 dark:border-gray-700 py-6 px-6">
<Button
onClick={handleUpload}
disabled={!skinFile || isUploading}
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white py-6 transition-all duration-300 transform hover:-translate-y-1 shadow-md hover:shadow-lg"
>
{isUploading ? '上传中...' : '上传皮肤'}
</Button>
</CardFooter>
</Card>
</div>
{/* 页脚 */}
<footer className="bg-gray-900/80 backdrop-blur-md text-gray-300 py-8 mt-16 border-t border-gray-800">
<div className="container mx-auto px-4 text-center relative z-10">
<p className="mb-2"> - Minecraft皮肤分享平台</p>
<p className="text-sm text-gray-400">© {new Date().getFullYear()} </p>
</div>
</footer>
</div> </div>
); );
} }

View File

@@ -1,12 +1,18 @@
// src/app/template.tsx // src/app/template.tsx
'use client'; 'use client';
import { useEffect } from 'react'; import { useEffect, Suspense } from 'react';
export default function Template({ children }: { children: React.ReactNode }) { export default function Template({ children }: { children: React.ReactNode }) {
useEffect(() => { useEffect(() => {
// 模板特定的效果 // 模板特定的效果
}, []); }, []);
return <>{children}</>; return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800">
<Suspense>
{children}
</Suspense>
</div>
);
} }

239
src/app/user-home/page.tsx Normal file
View File

@@ -0,0 +1,239 @@
// src/app/user-home/page.tsx
// 用户日常使用的主页面
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { getSession } from 'next-auth/react';
export default function UserHome() {
const [session, setSession] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSession = async () => {
try {
const sessionData = await getSession();
if (!sessionData) {
window.location.href = '/';
}
setSession(sessionData);
} catch (error) {
console.error('获取会话失败:', error);
window.location.href = '/';
} finally {
setLoading(false);
}
};
fetchSession();
}, []);
if (loading) {
return <div className="min-h-screen flex items-center justify-center">...</div>;
}
// 安全地获取用户信息
const userName = session?.user?.name || '玩家';
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800 relative overflow-hidden text-gray-900 dark:text-gray-100">
{/* 背景装饰元素 - 渐变模糊效果 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-20 -right-20 w-80 h-80 bg-emerald-400/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-20 -left-20 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl"></div>
<div className="absolute top-1/3 left-1/4 w-64 h-64 bg-emerald-300/10 rounded-full blur-3xl"></div>
</div>
{/* 主要内容区域 */}
<main className="container mx-auto px-4 py-12 relative z-10">
{/* 欢迎区域 */}
<section className="mb-16 max-w-4xl mx-auto">
<Card className="rounded-2xl overflow-hidden shadow-xl border border-emerald-200 dark:border-emerald-900/50 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardContent className="p-8">
<div className="flex flex-col md:flex-row items-center md:items-start gap-6">
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center text-white text-4xl shadow-lg">
{userName?.[0] || 'U'}
</div>
<div className="flex-1">
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 dark:text-white mb-2">
{userName}
</h1>
<p className="text-base md:text-lg opacity-90 leading-relaxed text-gray-600 dark:text-gray-400">Minecraft皮肤吧</p>
</div>
</div>
</CardContent>
</Card>
</section>
{/* 快速操作区域 */}
<section className="mb-16 max-w-4xl mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-5">
{/* 上传皮肤卡片 */}
<Link href="/skins/upload" className="block transform transition-all duration-300 hover:-translate-y-1">
<div className="bg-white/95 dark:bg-gray-800/95 p-6 rounded-2xl shadow-md hover:shadow-xl border border-emerald-100 dark:border-emerald-900/30 text-center relative overflow-hidden group backdrop-blur-sm">
<div className="absolute inset-0 bg-gradient-to-r from-emerald-50 to-teal-50 dark:from-emerald-900/10 dark:to-teal-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10">
<div className="text-5xl mb-4 transition-transform duration-300 group-hover:scale-110">📤</div>
<h3 className="font-bold text-lg mb-1 text-gray-800 dark:text-white"></h3>
<p className="text-sm text-gray-500 dark:text-gray-400"></p>
</div>
</div>
</Link>
{/* 我的皮肤卡片 */}
<Link href="/dashboard" className="block transform transition-all duration-300 hover:-translate-y-1">
<div className="bg-white/95 dark:bg-gray-800/95 p-6 rounded-2xl shadow-md hover:shadow-xl border border-emerald-100 dark:border-emerald-900/30 text-center relative overflow-hidden group backdrop-blur-sm">
<div className="absolute inset-0 bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-900/10 dark:to-cyan-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10">
<div className="text-5xl mb-4 transition-transform duration-300 group-hover:scale-110">👕</div>
<h3 className="font-bold text-lg mb-1 text-gray-800 dark:text-white"></h3>
<p className="text-sm text-gray-500 dark:text-gray-400"></p>
</div>
</div>
</Link>
{/* 外置登录卡片 */}
<div className="transform transition-all duration-300 hover:-translate-y-1" draggable="true" onDragStart={(e) => {
// 为拖拽提供Yggdrasil认证信息
const yggdrasilData = JSON.stringify({
name: "外置登录",
uuid: session?.user?.id || "user_uuid",
accessToken: "demo_token",
validateUrl: "http://localhost:3000/api/auth/validate",
refreshUrl: "http://localhost:3000/api/auth/refresh",
invalidateUrl: "http://localhost:3000/api/auth/invalidate",
userInfoUrl: "http://localhost:3000/api/auth/user",
authUrl: "http://localhost:3000/api/auth/authenticate"
});
e.dataTransfer.setData('text/plain', yggdrasilData);
e.dataTransfer.effectAllowed = 'copy';
}}>
<div className="bg-white/95 dark:bg-gray-800/95 p-6 rounded-2xl shadow-md hover:shadow-xl border border-emerald-100 dark:border-emerald-900/30 text-center relative overflow-hidden group backdrop-blur-sm cursor-grab active:cursor-grabbing">
<div className="absolute inset-0 bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/10 dark:to-indigo-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10">
<div className="text-5xl mb-4 transition-transform duration-300 group-hover:scale-110">🔐</div>
<h3 className="font-bold text-lg mb-1 text-gray-800 dark:text-white"></h3>
<p className="text-sm text-gray-500 dark:text-gray-400"></p>
</div>
{/* 拖拽提示 */}
<div className="absolute bottom-2 right-2 text-xs text-purple-500 dark:text-purple-400 opacity-70">
</div>
</div>
</div>
{/* 角色中心卡片 */}
<Link href="/character-center" className="block transform transition-all duration-300 hover:-translate-y-1">
<div className="bg-white/95 dark:bg-gray-800/95 p-6 rounded-2xl shadow-md hover:shadow-xl border border-emerald-100 dark:border-emerald-900/30 text-center relative overflow-hidden group backdrop-blur-sm">
<div className="absolute inset-0 bg-gradient-to-r from-amber-50 to-orange-50 dark:from-amber-900/10 dark:to-orange-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10">
<div className="text-5xl mb-4 transition-transform duration-300 group-hover:scale-110">👤</div>
<h3 className="font-bold text-lg mb-1 text-gray-800 dark:text-white"></h3>
<p className="text-sm text-gray-500 dark:text-gray-400"></p>
</div>
</div>
</Link>
</section>
{/* 问题帮助模块 */}
<section className="mb-16 max-w-4xl mx-auto">
<Card className="rounded-2xl overflow-hidden shadow-xl border border-emerald-200 dark:border-emerald-900/50 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardHeader className="bg-emerald-50/50 dark:bg-gray-800/80 border-b border-emerald-100 dark:border-gray-700">
<CardTitle className="text-xl text-gray-800 dark:text-white">使</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Link href="/help/basic" className="block">
<div className="h-full p-6 rounded-xl bg-gradient-to-br from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20 border border-blue-100 dark:border-blue-800/50 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
<h3 className="font-bold text-xl mb-3 text-blue-700 dark:text-blue-400"></h3>
<p className="text-gray-600 dark:text-gray-400 mb-4"></p>
<div className="flex justify-between items-center">
<span className="text-sm text-blue-600 dark:text-blue-500"></span>
<span className="text-blue-600 dark:text-blue-500"></span>
</div>
</div>
</Link>
<Link href="/help/yggdrasil" className="block">
<div className="h-full p-6 rounded-xl bg-gradient-to-br from-purple-50 to-violet-50 dark:from-purple-900/20 dark:to-violet-900/20 border border-purple-100 dark:border-purple-800/50 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
<h3 className="font-bold text-xl mb-3 text-purple-700 dark:text-purple-400">Yggdrasil教程</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">使Yggdrasil验证服务</p>
<div className="flex justify-between items-center">
<span className="text-sm text-purple-600 dark:text-purple-500"></span>
<span className="text-purple-600 dark:text-purple-500"></span>
</div>
</div>
</Link>
<Link href="/help/multilogin" className="block">
<div className="h-full p-6 rounded-xl bg-gradient-to-br from-amber-50 to-yellow-50 dark:from-amber-900/20 dark:to-yellow-900/20 border border-amber-100 dark:border-amber-800/50 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
<h3 className="font-bold text-xl mb-3 text-amber-700 dark:text-amber-400">MultiLogin教程</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">MultiLogin插件的配置与使用方法</p>
<div className="flex justify-between items-center">
<span className="text-sm text-amber-600 dark:text-amber-500"></span>
<span className="text-amber-600 dark:text-amber-500"></span>
</div>
</div>
</Link>
<Link href="/help/customskinloader" className="block">
<div className="h-full p-6 rounded-xl bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border border-green-100 dark:border-green-800/50 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
<h3 className="font-bold text-xl mb-3 text-green-700 dark:text-green-400">CustomSkinLoader教程</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">CustomSkinLoader</p>
<div className="flex justify-between items-center">
<span className="text-sm text-green-600 dark:text-green-500"></span>
<span className="text-green-600 dark:text-green-500"></span>
</div>
</div>
</Link>
</div>
</CardContent>
</Card>
</section>
{/* 统计信息区域 */}
<section className="mb-16 max-w-4xl mx-auto">
<Card className="rounded-2xl overflow-hidden shadow-xl border border-emerald-200 dark:border-emerald-900/50 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardHeader className="bg-emerald-50/50 dark:bg-gray-800/80 border-b border-emerald-100 dark:border-gray-700">
<CardTitle className="text-xl text-gray-800 dark:text-white"></CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div className="text-center p-4 rounded-xl bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-100 dark:border-emerald-900/30 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
<div className="text-4xl font-bold text-emerald-600 dark:text-emerald-400">1,256</div>
<div className="text-gray-600 dark:text-gray-400"></div>
</div>
<div className="text-center p-4 rounded-xl bg-teal-50 dark:bg-teal-900/20 border border-teal-100 dark:border-teal-900/30 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
<div className="text-4xl font-bold text-teal-600 dark:text-teal-400">342</div>
<div className="text-gray-600 dark:text-gray-400"></div>
</div>
<div className="text-center p-4 rounded-xl bg-cyan-50 dark:bg-cyan-900/20 border border-cyan-100 dark:border-cyan-900/30 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
<div className="text-4xl font-bold text-cyan-600 dark:text-cyan-400">12,890</div>
<div className="text-gray-600 dark:text-gray-400"></div>
</div>
<div className="text-center p-4 rounded-xl bg-lime-50 dark:bg-lime-900/20 border border-lime-100 dark:border-lime-900/30 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
<div className="text-4xl font-bold text-lime-600 dark:text-lime-400">89</div>
<div className="text-gray-600 dark:text-gray-400"></div>
</div>
</div>
</CardContent>
</Card>
</section>
</main>
{/* 页脚 */}
<footer className="bg-gray-900/80 backdrop-blur-md text-gray-300 py-8 border-t border-gray-800">
<div className="container mx-auto px-4 text-center relative z-10">
<p className="mb-2"> - Minecraft皮肤分享平台</p>
<p className="text-sm text-gray-400">© {new Date().getFullYear()} </p>
</div>
</footer>
</div>
);
}

View File

@@ -1,59 +1,275 @@
// src/components/Navbar.tsx // src/components/Navbar.tsx
'use client';
import Link from 'next/link'; import Link from 'next/link';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { getServerSession } from 'next-auth'; import { Session } from 'next-auth';
import { authOptions } from '@/lib/api/auth'; import { serverSignOut } from '@/lib/api/actions';
import { signOut } from '@/lib/api/auth'; import { useState, FunctionComponent } from 'react';
import { Menu, X } from 'lucide-react';
import Image from 'next/image';
export default async function Navbar() { // 定义组件Props接口
const session = await getServerSession(authOptions); interface NavbarProps {
session: Session | null;
}
// 处理客户端退出函数 // 移动端菜单组件
const handleSignOut = async () => { interface MobileMenuProps {
'use server'; session: Session | null;
await signOut(); userName: string;
}; serverSignOut: () => void;
handleProtectedLinkClick: () => void;
}
const MobileMenu: FunctionComponent<MobileMenuProps> = ({ session, userName, serverSignOut, handleProtectedLinkClick }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="md:hidden">
<Button
variant="ghost"
size="icon"
onClick={() => setIsOpen(!isOpen)}
className="text-white hover:bg-gray-700"
>
{isOpen ? <X size={24} /> : <Menu size={24} />}
</Button>
{/* 移动端下拉菜单 */}
{isOpen && (
<div className="absolute top-16 right-4 bg-gray-800 border border-gray-700 rounded-lg shadow-lg py-2 w-48 z-50">
<div className="space-y-2 px-2">
{session ? (
<>
<Link
href="/user-home"
className="block px-4 py-2 hover:bg-gray-700 rounded hover:text-emerald-400 transition-colors"
onClick={() => setIsOpen(false)}
>
</Link>
<Link
href="/skins"
className="block px-4 py-2 hover:bg-gray-700 rounded hover:text-emerald-400 transition-colors"
onClick={() => setIsOpen(false)}
>
</Link>
<Link
href="/character-center"
className="block px-4 py-2 hover:bg-gray-700 rounded hover:text-emerald-400 transition-colors"
onClick={() => setIsOpen(false)}
>
</Link>
</>
) : (
<>
<button
onClick={() => {
handleProtectedLinkClick();
setIsOpen(false);
}}
className="block w-full text-left px-4 py-2 hover:bg-gray-700 rounded cursor-pointer hover:text-emerald-400 transition-colors"
style={{ textDecoration: 'underline', textDecorationStyle: 'dotted' }}
>
</button>
<button
onClick={() => {
handleProtectedLinkClick();
setIsOpen(false);
}}
className="block w-full text-left px-4 py-2 hover:bg-gray-700 rounded cursor-pointer hover:text-emerald-400 transition-colors"
style={{ textDecoration: 'underline', textDecorationStyle: 'dotted' }}
>
</button>
<button
onClick={() => {
handleProtectedLinkClick();
setIsOpen(false);
}}
className="block w-full text-left px-4 py-2 hover:bg-gray-700 rounded cursor-pointer hover:text-emerald-400 transition-colors"
style={{ textDecoration: 'underline', textDecorationStyle: 'dotted' }}
>
</button>
</>
)}
{session && (
<div className="border-t border-gray-700 my-1 pt-2">
<p className="px-4 mb-2 text-sm text-gray-300">, {userName}</p>
<form action={serverSignOut} className="w-full">
<Button
variant="outline"
type="submit"
className="w-full text-red-300 border-white hover:bg-gray-700 hover:text-red-200"
>
退
</Button>
</form>
</div>
)}
{!session && (
<div className="border-t border-gray-700 my-1 pt-2 space-y-2">
<Button asChild variant="outline" className="w-full text-emerald-400 border-emerald-400/30 hover:bg-gray-700 hover:text-emerald-300 transition-all duration-300">
<Link href="/login" onClick={() => setIsOpen(false)}></Link>
</Button>
<Button asChild className="w-full bg-emerald-600 hover:bg-emerald-700 transition-all duration-300 transform hover:-translate-y-0.5">
<Link href="/register" onClick={() => setIsOpen(false)}></Link>
</Button>
</div>
)}
</div>
</div>
)}
</div>
);
};
// 移动Navbar组件到客户端
const Navbar: FunctionComponent<NavbarProps> = ({ session }) => {
// 添加日志以便调试
console.log('Navbar会话状态:', session ? '已登录' : '未登录');
console.log('会话用户:', session?.user);
// 安全地获取用户名 // 安全地获取用户名
const userName = session?.user?.name || '玩家'; const userName = session?.user?.name || '玩家';
// 控制登录提示弹窗的状态
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
// 处理需要登录的链接点击
const handleProtectedLinkClick = () => {
if (!session) {
setShowLoginPrompt(true);
}
};
return ( return (
<nav className="bg-gray-800 text-white py-4"> <>
<div className="container mx-auto px-4 flex justify-between items-center"> <nav className="bg-gray-900 text-white py-4 shadow-md backdrop-blur-sm bg-opacity-90 z-100">
<div className="flex items-center space-x-6"> <div className="container mx-auto px-4 flex justify-between items-center">
<Link href="/" className="text-xl font-bold"></Link> <div className="flex items-center space-x-6">
<div className="hidden md:flex space-x-4"> <Link href="/" className="flex items-center space-x-2 group">
<Link href="/dashboard" className="hover:text-gray-300"></Link> <Image
<Link href="/skins" className="hover:text-gray-300"></Link> src="/images/mc-favicon.ico"
alt="Logo"
width={32}
height={32}
className="rounded-xl w-8 h-8 transition-transform duration-300 group-hover:scale-110"
/>
<span className="text-xl font-bold bg-gradient-to-r from-emerald-400 to-teal-400 text-transparent bg-clip-text"></span>
</Link>
{/* 桌面端导航链接 */}
<div className="hidden md:flex space-x-4">
{session ? (
<>
<Link href="/user-home" className="hover:text-emerald-400 transition-colors"></Link>
<Link href="/skins" className="hover:text-emerald-400 transition-colors"></Link>
<Link href="/character-center" className="hover:text-emerald-400 transition-colors"></Link>
</>
) : (
<>
<button
onClick={handleProtectedLinkClick}
className="hover:text-emerald-400 cursor-pointer transition-colors"
style={{ textDecoration: 'underline', textDecorationStyle: 'dotted' }}
>
</button>
<button
onClick={handleProtectedLinkClick}
className="hover:text-emerald-400 cursor-pointer transition-colors"
style={{ textDecoration: 'underline', textDecorationStyle: 'dotted' }}
>
</button>
<button
onClick={handleProtectedLinkClick}
className="hover:text-emerald-400 cursor-pointer transition-colors"
style={{ textDecoration: 'underline', textDecorationStyle: 'dotted' }}
>
</button>
</>
)}
</div>
</div> </div>
</div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{session ? ( {/* 桌面端登录状态 */}
<div className="flex items-center space-x-4"> <div className="hidden md:flex items-center space-x-4">
<span>, {userName}</span> {session && <span>, {userName}</span>}
<form action={handleSignOut}> {session ? (
<Button <form action={serverSignOut}>
variant="outline" <Button
variant="outline"
type="submit" type="submit"
className="text-white border-white hover:bg-gray-700" className="text-red-300 border-white hover:bg-gray-700 hover:text-red-200 transition-all duration-300"
> >
退 退
</Button> </Button>
</form> </form>
) : (
<>
<Button asChild variant="outline" className="text-emerald-400 border-emerald-400/30 hover:bg-gray-700 hover:text-emerald-300 transition-all duration-300">
<Link href="/login"></Link>
</Button>
<Button asChild className="bg-emerald-600 hover:bg-emerald-700 transition-all duration-300 transform hover:-translate-y-0.5">
<Link href="/register"></Link>
</Button>
</>
)}
</div> </div>
) : (
<div className="flex space-x-2"> {/* 移动端菜单按钮 */}
<Button asChild variant="outline" className="text-white border-white hover:bg-gray-700"> <MobileMenu session={session} userName={userName} serverSignOut={serverSignOut} handleProtectedLinkClick={handleProtectedLinkClick} />
</div>
</div>
</nav>
{/* 登录提示弹窗 */}
{showLoginPrompt && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-fadeIn"
onClick={() => setShowLoginPrompt(false)}
>
<div
className="bg-white/95 dark:bg-gray-800/95 backdrop-blur-md rounded-xl shadow-2xl p-8 max-w-md w-full mx-4 transform transition-all duration-300 animate-slideUp"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4 text-center"></h3>
<p className="text-gray-700 dark:text-gray-300 mb-6 text-center">
访
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
asChild
className="bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl px-6 py-2.5 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1"
onClick={() => setShowLoginPrompt(false)}
>
<Link href="/login"></Link> <Link href="/login"></Link>
</Button> </Button>
<Button asChild className="bg-green-600 hover:bg-green-700"> <Button
asChild
variant="outline"
className="border-emerald-600 text-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-900/10 rounded-xl px-6 py-2.5 shadow-md hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1"
onClick={() => setShowLoginPrompt(false)}
>
<Link href="/register"></Link> <Link href="/register"></Link>
</Button> </Button>
</div> </div>
)} </div>
</div> </div>
</div> )}
</nav> </>
); );
} };
// 直接导出客户端Navbar组件会话获取将由上层组件处理
export default Navbar;

View File

@@ -0,0 +1,259 @@
// src/components/ServerInfoBubble.tsx
'use client';
import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
interface ServerInfoBubbleProps {
zIndex?: number;
dragDistanceLimit?: number;
}
export default function ServerInfoBubble({
zIndex = 1000,
dragDistanceLimit = 200
}: ServerInfoBubbleProps) {
// 服务器信息,这些将轮流显示
const serverInfo = {
players: {
title: '在线玩家',
content: '服务器内有 {playerCount} 名玩家正在游玩'
},
address: {
title: '服务器地址',
content: 'mc.hitwh.edu.cn:25565'
},
description: {
title: '服务器介绍',
content: '欢迎来到HITWH Minecraft服务器这是一个充满创造力的社区'
}
};
const [currentInfo, setCurrentInfo] = useState<'players' | 'address' | 'description'>('players');
const [displayText, setDisplayText] = useState('');
const [cursorVisible, setCursorVisible] = useState(true);
const [isTyping, setIsTyping] = useState(true);
const [playerCount, setPlayerCount] = useState(0); // 模拟玩家数量
// 拖拽相关状态
const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const initialDragPos = useRef({ x: 0, y: 0 });
const bubbleRef = useRef<HTMLDivElement>(null);
// 随机生成玩家数量(模拟从服务器获取)
useEffect(() => {
const updatePlayerCount = () => {
setPlayerCount(Math.floor(Math.random() * 50) + 1); // 1-50随机玩家数
};
updatePlayerCount();
const countInterval = setInterval(updatePlayerCount, 30000); // 每30秒更新一次
return () => clearInterval(countInterval);
}, []);
// 打字机效果
useEffect(() => {
if (!isTyping) return;
const fullText = serverInfo[currentInfo].content.replace('{playerCount}', playerCount.toString());
let index = 0;
const typeInterval = setInterval(() => {
if (index < fullText.length) {
setDisplayText(fullText.slice(0, index + 1));
index++;
} else {
clearInterval(typeInterval);
setIsTyping(false);
}
}, 50); // 打字速度
return () => clearInterval(typeInterval);
}, [currentInfo, isTyping, playerCount]);
// 光标闪烁效果
useEffect(() => {
const cursorInterval = setInterval(() => {
setCursorVisible(prev => !prev);
}, 530);
return () => clearInterval(cursorInterval);
}, []);
// 轮流显示不同信息
useEffect(() => {
const infoInterval = setInterval(() => {
setIsTyping(true);
setDisplayText('');
// 切换到下一个信息
if (currentInfo === 'players') {
setCurrentInfo('address');
} else if (currentInfo === 'address') {
setCurrentInfo('description');
} else {
setCurrentInfo('players');
}
}, 8000); // 每个信息显示8秒
return () => clearInterval(infoInterval);
}, [currentInfo]);
// 动态计算右下角位置
const [screenPosition, setScreenPosition] = useState({ bottom: 16, right: 16 });
// 监听窗口大小变化,重新计算位置
useEffect(() => {
const updatePosition = () => {
setScreenPosition({ bottom: 16, right: 16 });
};
// 初始计算位置
updatePosition();
// 监听窗口大小变化
window.addEventListener('resize', updatePosition);
return () => {
window.removeEventListener('resize', updatePosition);
};
}, []);
// 开始拖拽
const startDragging = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
initialDragPos.current = {
x: e.clientX - dragPosition.x,
y: e.clientY - dragPosition.y
};
// 暂停动画并改变光标
if (bubbleRef.current) {
bubbleRef.current.style.animationPlayState = 'paused';
bubbleRef.current.style.cursor = 'grabbing';
}
};
// 结束拖拽并返回原位
const endDragging = () => {
if (!isDragging) return;
setIsDragging(false);
// 恢复动画和光标
if (bubbleRef.current) {
bubbleRef.current.style.animationPlayState = 'running';
bubbleRef.current.style.cursor = 'move';
}
// 使用CSS动画返回原位
if (bubbleRef.current) {
bubbleRef.current.style.transition = 'transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1)';
bubbleRef.current.style.transform = 'translate(0, 0)';
// 更新状态并清除过渡效果
setTimeout(() => {
setDragPosition({ x: 0, y: 0 });
if (bubbleRef.current) {
bubbleRef.current.style.transition = '';
}
}, 800);
}
};
// 处理拖拽移动
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const newX = e.clientX - initialDragPos.current.x;
const newY = e.clientY - initialDragPos.current.y;
// 限制拖拽范围
const limitedX = Math.max(-dragDistanceLimit, Math.min(dragDistanceLimit, newX));
const limitedY = Math.max(-dragDistanceLimit, Math.min(dragDistanceLimit, newY));
// 直接操作DOM进行拖动避免频繁的状态更新
if (bubbleRef.current) {
bubbleRef.current.style.transform = `translate(${limitedX}px, ${limitedY}px)`;
}
// 同时更新状态以便组件状态一致
setDragPosition({ x: limitedX, y: limitedY });
};
// 设置全局鼠标事件监听
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', endDragging);
document.addEventListener('mouseleave', endDragging);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', endDragging);
document.removeEventListener('mouseleave', endDragging);
};
}
}, [isDragging, dragDistanceLimit]);
return (
<div
ref={bubbleRef}
className="fixed flex flex-col sm:flex-row gap-4 animate-float cursor-move"
style={{
zIndex,
bottom: `${screenPosition.bottom}px`,
right: `${screenPosition.right}px`,
userSelect: 'none',
touchAction: 'none',
pointerEvents: 'all',
transform: `translate(${dragPosition.x}px, ${dragPosition.y}px)`
}}
onMouseDown={startDragging}
>
{/* 服务器头像 */}
<div className="relative">
<div className="absolute -inset-1 bg-gradient-to-r from-emerald-400 to-teal-500 rounded-full blur-md opacity-75"></div>
<div className="relative w-16 h-16 rounded-full bg-gray-900 overflow-hidden border-2 border-white dark:border-gray-800 shadow-lg">
<div className="w-full h-full flex items-center justify-center bg-emerald-600 text-white font-bold text-xl">
</div>
</div>
</div>
{/* 对话框气泡 */}
<div className="relative bg-white dark:bg-gray-800 rounded-2xl p-4 max-w-xs shadow-xl border border-emerald-200 dark:border-emerald-900/30">
{/* 气泡尾部 */}
<div className="absolute top-1/2 transform -translate-y-1/2 w-4 h-4 bg-white dark:bg-gray-800 border-t border-l border-emerald-200 dark:border-emerald-900/30 left-0 -translate-x-2 rotate-45"></div>
{/* 气泡内容 */}
<div className="font-medium text-emerald-600 dark:text-emerald-400 mb-2">
{serverInfo[currentInfo].title}
</div>
<div className="text-sm text-gray-700 dark:text-gray-300 relative min-h-[2rem]">
{displayText}
<span className={`inline-block w-2 h-4 ml-0.5 bg-gray-500 dark:bg-gray-400 transition-opacity ${cursorVisible && isTyping ? 'opacity-100' : 'opacity-0'}`}></span>
</div>
</div>
</div>
);
}
// 添加浮动动画样式
export const ServerInfoBubbleStyles = `
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
.animate-float {
animation: float 6s ease-in-out infinite;
}`;

View File

@@ -1,9 +1,11 @@
// src/components/auth/AuthForm.tsx // src/components/auth/AuthForm.tsx
// 认证表单组件 - 包含登录和注册功能,同时提供测试账号信息
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { signIn } from 'next-auth/react';
interface AuthFormProps { interface AuthFormProps {
type: 'login' | 'register'; type: 'login' | 'register';
@@ -20,12 +22,30 @@ export default function AuthForm({ type }: AuthFormProps) {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
// 这里应该调用认证API try {
console.log('提交表单', { username, password, email, minecraftUsername }); if (type === 'login') {
// 使用next-auth的signIn功能进行登录
// 模拟API请求 const result = await signIn('credentials', {
await new Promise(resolve => setTimeout(resolve, 1000)); username: username || email, // 使用username或email作为用户名字段
setIsLoading(false); email: email, // 传递email字段
password,
redirect: true, // 登录成功后自动重定向
callbackUrl: '/user-home' // 指定重定向目标
});
console.log('登录结果:', result);
} else {
// 注册逻辑可以在这里实现
console.log('注册信息', { username, password, email, minecraftUsername });
// 模拟注册请求
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (error) {
console.error('认证失败:', error);
// 这里可以添加错误处理逻辑
} finally {
setIsLoading(false);
}
}; };
return ( return (
@@ -35,11 +55,24 @@ export default function AuthForm({ type }: AuthFormProps) {
<Input <Input
id="username" id="username"
type="text" type="text"
required
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
placeholder={type === 'login' ? "用户名或邮箱" : "输入用户名"}
/> />
</div> </div>
{type === 'login' && (
<div>
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="或直接输入邮箱登录"
/>
</div>
)}
<div> <div>
<Label htmlFor="password"></Label> <Label htmlFor="password"></Label>
<Input <Input
@@ -79,6 +112,14 @@ export default function AuthForm({ type }: AuthFormProps) {
<Button type="submit" className="w-full" disabled={isLoading}> <Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? '处理中...' : type === 'login' ? '登录' : '注册'} {isLoading ? '处理中...' : type === 'login' ? '登录' : '注册'}
</Button> </Button>
{type === 'login' && (
<div className="mt-4 p-3 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg text-sm border border-emerald-200 dark:border-emerald-800">
<div className="font-medium mb-1 text-emerald-700 dark:text-emerald-400"></div>
<div>: <span className="font-mono bg-white dark:bg-gray-800 px-1.5 py-0.5 rounded">test</span></div>
<div>: <span className="font-mono bg-white dark:bg-gray-800 px-1.5 py-0.5 rounded">test</span></div>
</div>
)}
</form> </form>
); );
} }

View File

@@ -0,0 +1,81 @@
// Canvas 2D皮肤预览组件 - 只渲染头部正脸
'use client';
import React, { useRef, useEffect } from 'react';
interface Canvas2DSkinPreviewProps {
skinUrl: string;
size?: number;
className?: string;
}
const Canvas2DSkinPreview: React.FC<Canvas2DSkinPreviewProps> = ({
skinUrl,
size = 128,
className = ''
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 设置canvas尺寸
canvas.width = size;
canvas.height = size;
// 清空canvas
ctx.clearRect(0, 0, size, size);
// 创建图像对象加载皮肤
const skinImage = new Image();
skinImage.src = skinUrl;
skinImage.crossOrigin = 'anonymous'; // 确保可以跨域访问
skinImage.onload = () => {
// 确保使用像素化渲染
ctx.imageSmoothingEnabled = false;
// 皮肤头部正脸坐标64x64皮肤
const sourceX = 8;
const sourceY = 8;
const sourceSize = 8;
// 计算缩放比例
const scale = size / sourceSize;
// 绘制皮肤头部正脸(像素化渲染)
ctx.drawImage(
skinImage,
sourceX, // 源图像的x坐标
sourceY, // 源图像的y坐标
sourceSize, // 源图像的宽度
sourceSize, // 源图像的高度
0, // 目标canvas的x坐标
0, // 目标canvas的y坐标
size, // 目标canvas的宽度
size // 目标canvas的高度
);
};
// 错误处理
skinImage.onerror = () => {
console.error('无法加载皮肤图片:', skinUrl);
// 绘制一个默认的灰色方块
ctx.fillStyle = '#cccccc';
ctx.fillRect(0, 0, size, size);
};
}, [skinUrl, size]);
return (
<canvas
ref={canvasRef}
className={`image-rendering-pixelated ${className}`}
style={{ imageRendering: 'pixelated' }} // 确保所有浏览器都使用像素化渲染
/>
);
};
export default Canvas2DSkinPreview;

View File

@@ -1,6 +1,7 @@
// src/components/skins/SkinGrid.tsx // src/components/skins/SkinGrid.tsx
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import Canvas2DSkinPreview from './Canvas2DSkinPreview';
interface Skin { interface Skin {
id: string; id: string;
@@ -10,19 +11,34 @@ interface Skin {
export default function SkinGrid({ skins }: { skins: Skin[] }) { export default function SkinGrid({ skins }: { skins: Skin[] }) {
if (skins.length === 0) { if (skins.length === 0) {
return <p></p>; return null; // 空状态由父组件处理
} }
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{skins.map((skin) => ( {skins.map((skin) => (
<Link key={skin.id} href={`/skins/${skin.id}`}> <Link
<Card className="hover:shadow-lg transition-shadow"> key={skin.id}
<CardHeader> href={`/skins/${skin.id}`}
<CardTitle>{skin.name}</CardTitle> className="block transform transition-all duration-300 hover:-translate-y-1"
>
<Card className="h-full border border-gray-100 dark:border-gray-700 overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-emerald-900/5 dark:to-teal-900/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="aspect-square bg-gray-50 dark:bg-gray-900 flex items-center justify-center p-4 relative">
<div className="absolute inset-0 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<Canvas2DSkinPreview
skinUrl={`/test-skin.png`}
size={128}
className="max-w-full max-h-full relative z-10 transition-transform duration-500 group-hover:scale-110"
/>
</div>
<CardHeader className="relative z-10 pb-2">
<CardTitle className="text-lg font-bold text-gray-800 dark:text-white">{skin.name}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="relative z-10">
<p className="text-gray-500">: {skin.createdAt}</p> <p className="text-sm text-gray-500 dark:text-gray-400">: {skin.createdAt}</p>
</CardContent> </CardContent>
</Card> </Card>
</Link> </Link>

View File

@@ -1,18 +0,0 @@
// src/components/skins/SkinUploader.tsx
import Image from 'next/image';
export default function SkinUploader({ file }: { file: File }) {
const imageUrl = URL.createObjectURL(file);
return (
<div className="border rounded-md p-2 bg-gray-50">
<Image
src={imageUrl}
alt="皮肤预览"
width={128}
height={64}
className="object-contain mx-auto"
/>
</div>
);
}

View File

@@ -1,81 +0,0 @@
// src/components/skins/SkinViewer3D.tsx
//这部分还没写完!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'use client';
import { extend, Canvas } from '@react-three/fiber';
import { OrbitControls, useTexture } from '@react-three/drei';
import { Suspense } from 'react';
import * as THREE from 'three';
// 显式扩展 Three.js 类
extend({
Mesh: THREE.Mesh,
BoxGeometry: THREE.BoxGeometry,
MeshStandardMaterial: THREE.MeshStandardMaterial,
AmbientLight: THREE.AmbientLight,
SpotLight: THREE.SpotLight,
MeshBasicMaterial: THREE.MeshBasicMaterial
});
interface SkinModelProps {
skinUrl: string;
}
const SkinModel: React.FC<SkinModelProps> = ({ skinUrl }) => {
const texture = useTexture(skinUrl);
return (
<mesh rotation={[0, Math.PI / 4, 0]}>
<boxGeometry args={[1, 2, 0.5]} />
<meshStandardMaterial map={texture} side={THREE.DoubleSide} />
</mesh>
);
};
const FallbackModel: React.FC = () => (
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshBasicMaterial color="#cccccc" wireframe opacity={0.5} transparent />
</mesh>
);
interface SkinViewer3DProps {
skinUrl: string;
}
const SkinViewer3D: React.FC<SkinViewer3DProps> = ({ skinUrl }) => {
return (
<div className="w-full h-96 bg-gray-100 dark:bg-gray-800 rounded-lg">
<Canvas
camera={{
position: [3, 1.5, 3],
fov: 50,
near: 0.1,
far: 100
}}
shadows
>
<ambientLight intensity={0.7} />
<spotLight
position={[5, 8, 5]}
angle={0.3}
penumbra={1}
intensity={1.5}
castShadow
shadow-mapSize-width={1024}
shadow-mapSize-height={1024}
/>
<Suspense fallback={<FallbackModel />}>
<SkinModel skinUrl={skinUrl} />
<OrbitControls
enableZoom={true}
enablePan={true}
minPolarAngle={Math.PI / 6}
maxPolarAngle={Math.PI / 1.8}
/>
</Suspense>
</Canvas>
</div>
);
};
export default SkinViewer3D;

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-gray-200 dark:border-gray-700 py-6 shadow-lg relative transition-all duration-300 hover:-translate-y-1 hover:shadow-xl",
className className
)} )}
{...props} {...props}

87
src/lib/api/actions.ts Normal file
View File

@@ -0,0 +1,87 @@
// src/lib/api/actions.ts
'use server';
import axios from 'axios';
const API_URL = process.env.NEXT_PUBLIC_API_URL || '/api';
// 导出退出登录函数 - 供服务器端使用
export const serverSignOut = async () => {
console.log('serverSignOut函数执行开始');
// 使用更直接的方式清除会话并重定向
// 1. 首先清除所有可能的cookie
const { cookies } = await import('next/headers');
const cookieStore = cookies();
// 获取并记录当前所有的cookie
const allCookies = cookieStore.getAll();
console.log('当前cookies:', allCookies.map(c => c.name));
// 清除所有可能的NextAuth相关cookie
let deletedCookies = 0;
allCookies.forEach(cookie => {
if (cookie.name.includes('next-auth')) {
console.log('删除cookie:', cookie.name);
cookieStore.delete(cookie.name);
deletedCookies++;
}
});
console.log(`总共删除了${deletedCookies}个cookie`);
// 强制重定向到登录页面
// 确保页面完全刷新而不是客户端导航
console.log('执行重定向到登录页面');
const { redirect } = await import('next/navigation');
redirect('/login');
};
// 导出登录函数 - 供服务器端使用
export const login = async (credentials: {
username?: string;
email?: string;
password: string
}) => {
try {
// 对于测试环境,可以直接验证测试账号
const TEST_USERNAME = 'test';
const TEST_PASSWORD = 'test';
// 对于测试环境,可以直接验证测试账号 - 支持通过username或email字段登录
const usernameField = credentials.username || credentials.email;
if (usernameField === TEST_USERNAME && credentials.password === TEST_PASSWORD) {
return {
success: true,
user: {
id: 'test_user_1',
name: '测试玩家',
email: 'test@test.com',
minecraftUsername: 'SteveTest'
}
};
}
// 实际环境中调用API
const response = await axios.post(`${API_URL}/auth/login`, credentials);
return { success: true, ...response.data };
} catch (error) {
console.error('登录失败:', error);
return { success: false, error: '登录失败,请检查用户名和密码' };
}
};
// 导出注册函数
export const register = async (userData: {
username: string;
password: string;
email: string;
minecraftUsername: string;
}) => {
try {
// 实际环境中调用API
const response = await axios.post(`${API_URL}/auth/register`, userData);
return { success: true, ...response.data };
} catch (error) {
console.error('注册失败:', error);
return { success: false, error: '注册失败,请稍后再试' };
}
};

View File

@@ -22,10 +22,31 @@ export const authOptions: AuthOptions = {
name: 'Credentials', name: 'Credentials',
credentials: { credentials: {
username: { label: "用户名", type: "text" }, username: { label: "用户名", type: "text" },
email: { label: "邮箱", type: "email" },
password: { label: "密码", type: "password" } password: { label: "密码", type: "password" }
}, },
async authorize(credentials) { async authorize(credentials) {
// 默认测试账号 - 用于开发和测试环境
const TEST_USERNAME = 'test';
const TEST_PASSWORD = 'test';
try { try {
// 检查是否是测试账号 - 支持通过username或email字段登录
const usernameField = credentials?.username || credentials?.email;
if (usernameField === TEST_USERNAME && credentials?.password === TEST_PASSWORD) {
// 返回模拟的测试用户数据
return {
id: 'test_user_1',
name: '测试玩家',
email: 'test@test.com',
minecraftUsername: 'SteveTest'
};
}
// 正常的API登录流程
const response = await axios.post(`${API_URL}/auth/login`, { const response = await axios.post(`${API_URL}/auth/login`, {
username: credentials?.username, username: credentials?.username,
password: credentials?.password password: credentials?.password
@@ -48,7 +69,7 @@ export const authOptions: AuthOptions = {
], ],
pages: { pages: {
signIn: '/login', signIn: '/login',
signOut: '/login' signOut: '/login' // 退出登录后重定向到登录页面
}, },
callbacks: { callbacks: {
async jwt({ token, user }) { async jwt({ token, user }) {
@@ -62,6 +83,15 @@ export const authOptions: AuthOptions = {
session.user.id = token.id as string; session.user.id = token.id as string;
} }
return session; return session;
},
// 登录成功后重定向到用户主页
async redirect({ url, baseUrl }) {
// 如果已经有明确的重定向URL则使用它
if (url.startsWith(baseUrl)) {
return url;
}
// 否则默认重定向到用户主页
return `${baseUrl}/user-home`;
} }
}, },
secret: process.env.NEXTAUTH_SECRET, secret: process.env.NEXTAUTH_SECRET,
@@ -70,25 +100,4 @@ export const authOptions: AuthOptions = {
// 导出 NextAuth 处理函数 // 导出 NextAuth 处理函数
export const nextAuthHandler = NextAuth(authOptions); export const nextAuthHandler = NextAuth(authOptions);
// 导出登录函数 // Server Actions已移至actions.ts文件
export const login = async (credentials: {
username: string;
password: string
}) => {
// 实现同上...
};
// 导出注册函数
export const register = async (userData: {
username: string;
password: string;
email: string;
minecraftUsername: string;
}) => {
// 实现同上...
};
export async function signOut() {
// 实际应用中这里会调用API退出登录
console.log('用户退出登录');
// 在客户端组件中,你可能会使用 next-auth 的 signOut 方法
}

View File

View File

@@ -1,33 +0,0 @@
/* src/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 添加 Minecraft 像素风格字体 */
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
/* 自定义全局样式 */
@layer base {
body {
@apply bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100;
font-family: 'Inter', sans-serif;
}
h1, h2, h3, h4, h5, h6 {
@apply font-bold;
}
/* Minecraft 风格按钮 */
.btn-minecraft {
@apply px-4 py-2 rounded-md font-bold text-white bg-minecraft-green
border-2 border-b-4 border-minecraft-dark-green
hover:bg-minecraft-dark-green hover:border-minecraft-dark
active:transform active:translate-y-1 active:border-b-2
transition-all duration-100;
}
/* 像素风格元素 */
.pixel-border {
@apply border-2 border-gray-800;
}
}

19
src/types/global.d.ts vendored
View File

@@ -1,17 +1,8 @@
// src/types/global.d.ts // src/types/global.d.ts
import * as THREE from 'three';
import { Object3DNode } from '@react-three/fiber';
declare global { declare global {
namespace JSX { interface Window {
interface IntrinsicElements { __NEXT_DATA__?: any;
// Three.js 元素 gtag?: (...args: any[]) => void;
mesh: Object3DNode<THREE.Mesh, typeof THREE.Mesh>; // 可以在这里扩展window对象的类型
boxGeometry: Object3DNode<THREE.BoxGeometry, typeof THREE.BoxGeometry>; }
meshStandardMaterial: Object3DNode<THREE.MeshStandardMaterial, typeof THREE.MeshStandardMaterial>;
ambientLight: Object3DNode<THREE.AmbientLight, typeof THREE.AmbientLight>;
spotLight: Object3DNode<THREE.SpotLight, typeof THREE.SpotLight>;
meshBasicMaterial: Object3DNode<THREE.MeshBasicMaterial, typeof THREE.MeshBasicMaterial>;
}
}
} }

17
src/types/three.d.ts vendored
View File

@@ -1,17 +0,0 @@
// src/types/three.d.ts
import * as THREE from 'three';
import { Object3DNode } from '@react-three/fiber';
declare global {
namespace JSX {
interface IntrinsicElements {
// 使用大写开头的类名
mesh: Object3DNode<THREE.Mesh, typeof THREE.Mesh>;
boxGeometry: Object3DNode<THREE.BoxGeometry, typeof THREE.BoxGeometry>;
meshStandardMaterial: Object3DNode<THREE.MeshStandardMaterial, typeof THREE.MeshStandardMaterial>;
ambientLight: Object3DNode<THREE.AmbientLight, typeof THREE.AmbientLight>;
spotLight: Object3DNode<THREE.SpotLight, typeof THREE.SpotLight>;
meshBasicMaterial: Object3DNode<THREE.MeshBasicMaterial, typeof THREE.MeshBasicMaterial>;
}
}
}

114
数据库参考.txt Normal file
View File

@@ -0,0 +1,114 @@
-- 用户表,支持积分系统和权限管理
CREATE TABLE `user` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名',
`password` VARCHAR(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码哈希',
`email` VARCHAR(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '邮箱地址',
`avatar` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '头像URL存储在MinIO中',
`points` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户积分',
`role` VARCHAR(50) NOT NULL DEFAULT 'user' COMMENT '用户角色user, vip, admin等',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '用户状态1:正常, 0:禁用, -1:删除)',
`last_login_at` TIMESTAMP NULL COMMENT '最后登录时间',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
UNIQUE KEY `uk_email` (`email`),
INDEX `idx_role` (`role`),
INDEX `idx_status` (`status`),
INDEX `idx_points` (`points` DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';
-- 材质表,存储皮肤和披风
CREATE TABLE `textures` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '材质的唯一ID',
`uploader_id` BIGINT UNSIGNED NOT NULL COMMENT '上传者的用户ID',
`name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '材质名称',
`description` TEXT COMMENT '材质描述',
`type` ENUM('SKIN', 'CAPE') NOT NULL COMMENT '材质类型(皮肤或披风)',
`url` VARCHAR(255) NOT NULL COMMENT '材质在MinIO中的永久访问URL',
`hash` VARCHAR(64) NOT NULL COMMENT '材质文件的SHA-256哈希值用于快速去重和校验',
`size` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
`is_public` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否公开到皮肤广场',
`download_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '下载次数',
`favorite_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '收藏次数',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态1:正常, 0:审核中, -1:已删除)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_hash` (`hash`),
INDEX `idx_uploader_id` (`uploader_id`),
INDEX `idx_public_type_status` (`is_public`, `type`, `status`),
INDEX `idx_download_count` (`download_count` DESC),
INDEX `idx_favorite_count` (`favorite_count` DESC),
CONSTRAINT `fk_textures_uploader` FOREIGN KEY (`uploader_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='皮肤与披风材质表';
-- 用户材质收藏表
CREATE TABLE `user_texture_favorites` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '收藏记录的唯一ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`texture_id` BIGINT UNSIGNED NOT NULL COMMENT '收藏的材质ID',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_texture` (`user_id`, `texture_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_texture_id` (`texture_id`),
INDEX `idx_created_at` (`created_at`),
CONSTRAINT `fk_favorites_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_favorites_texture` FOREIGN KEY (`texture_id`) REFERENCES `textures` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户材质收藏表';
-- 用户角色信息表Minecraft档案
CREATE TABLE `profiles` (
`uuid` VARCHAR(36) NOT NULL COMMENT '角色的UUID通常为Minecraft玩家的UUID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '关联的用户ID',
`name` VARCHAR(16) NOT NULL COMMENT '角色名Minecraft游戏内名称',
`skin_id` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '当前使用的皮肤ID',
`cape_id` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '当前使用的披风ID',
`rsa_private_key` TEXT NOT NULL COMMENT '用于签名的RSA-2048私钥PEM格式',
`is_active` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否为活跃档案',
`last_used_at` TIMESTAMP NULL COMMENT '最后使用时间',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`uuid`),
UNIQUE KEY `uk_name` (`name`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_active` (`is_active`),
CONSTRAINT `fk_profiles_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_profiles_skin` FOREIGN KEY (`skin_id`) REFERENCES `textures` (`id`) ON DELETE SET NULL,
CONSTRAINT `fk_profiles_cape` FOREIGN KEY (`cape_id`) REFERENCES `textures` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色信息表Minecraft档案';
-- Casbin权限管理相关表
-- casbin_rule表用于存储RBAC权限规则
CREATE TABLE `casbin_rule` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '规则ID',
`ptype` VARCHAR(100) NOT NULL COMMENT '策略类型p, g等',
`v0` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '主体(用户或角色)',
`v1` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '资源对象',
-- 材质表,存储皮肤和披风
CREATE TABLE `textures` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '材质的唯一ID',
`uploader_id` BIGINT UNSIGNED NOT NULL COMMENT '上传者的用户ID',
`name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '材质名称',
`description` TEXT COMMENT '材质描述',
`type` ENUM('SKIN', 'CAPE') NOT NULL COMMENT '材质类型(皮肤或披风)',
`url` VARCHAR(255) NOT NULL COMMENT '材质在MinIO中的永久访问URL',
`hash` VARCHAR(64) NOT NULL COMMENT '材质文件的SHA-256哈希值用于快速去重和校验',
`size` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
`is_public` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否公开到皮肤广场',
`download_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '下载次数',
`favorite_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '收藏次数',
"is_silm" BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否为细手臂模型Steve/Alex默认为粗手臂模型Steve',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态1:正常, 0:审核中, -1:已删除)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_hash` (`hash`),
INDEX `idx_uploader_id` (`uploader_id`),
INDEX `idx_public_type_status` (`is_public`, `type`, `status`),
INDEX `idx_download_count` (`download_count` DESC),
INDEX `idx_favorite_count` (`favorite_count` DESC),
CONSTRAINT `fk_textures_uploader` FOREIGN KEY (`uploader_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='皮肤与披风材质表';