first save
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
// src/components/Navbar.tsx
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/api/auth';
|
||||
import { signOut } from '@/lib/api/auth';
|
||||
|
||||
export default async function Navbar() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
// 处理客户端退出函数
|
||||
const handleSignOut = async () => {
|
||||
'use server';
|
||||
await signOut();
|
||||
};
|
||||
|
||||
// 安全地获取用户名
|
||||
const userName = session?.user?.name || '玩家';
|
||||
|
||||
return (
|
||||
<nav className="bg-gray-800 text-white py-4">
|
||||
<div className="container mx-auto px-4 flex justify-between items-center">
|
||||
<div className="flex items-center space-x-6">
|
||||
<Link href="/" className="text-xl font-bold">我的世界皮肤库</Link>
|
||||
<div className="hidden md:flex space-x-4">
|
||||
<Link href="/dashboard" className="hover:text-gray-300">仪表盘</Link>
|
||||
<Link href="/skins" className="hover:text-gray-300">皮肤库</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{session ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
<span>你好, {userName}</span>
|
||||
<form action={handleSignOut}>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="submit"
|
||||
className="text-white border-white hover:bg-gray-700"
|
||||
>
|
||||
退出登录
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex space-x-2">
|
||||
<Button asChild variant="outline" className="text-white border-white hover:bg-gray-700">
|
||||
<Link href="/login">登录</Link>
|
||||
</Button>
|
||||
<Button asChild className="bg-green-600 hover:bg-green-700">
|
||||
<Link href="/register">注册</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// src/components/auth/AuthForm.tsx
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
interface AuthFormProps {
|
||||
type: 'login' | 'register';
|
||||
}
|
||||
|
||||
export default function AuthForm({ type }: AuthFormProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [minecraftUsername, setMinecraftUsername] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
// 这里应该调用认证API
|
||||
console.log('提交表单', { username, password, email, minecraftUsername });
|
||||
|
||||
// 模拟API请求
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="username">用户名</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="password">密码</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{type === 'register' && (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="email">电子邮箱</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="minecraftUsername">Minecraft 用户名</Label>
|
||||
<Input
|
||||
id="minecraftUsername"
|
||||
type="text"
|
||||
required
|
||||
value={minecraftUsername}
|
||||
onChange={(e) => setMinecraftUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? '处理中...' : type === 'login' ? '登录' : '注册'}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// src/components/skins/SkinGrid.tsx
|
||||
import Link from 'next/link';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
interface Skin {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function SkinGrid({ skins }: { skins: Skin[] }) {
|
||||
if (skins.length === 0) {
|
||||
return <p>你还没有上传任何皮肤</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{skins.map((skin) => (
|
||||
<Link key={skin.id} href={`/skins/${skin.id}`}>
|
||||
<Card className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle>{skin.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-500">创建于: {skin.createdAt}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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;
|
||||
59
src/components/ui/button.tsx
Normal file
59
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
Reference in New Issue
Block a user