- {texture.uploader && (
+ {texture.uploader_username && (
上传者:
- {texture.uploader.username}
+ {texture.uploader_username}
)}
diff --git a/src/components/SkinViewer.tsx b/src/components/SkinViewer.tsx
index 839467a..2e1170d 100644
--- a/src/components/SkinViewer.tsx
+++ b/src/components/SkinViewer.tsx
@@ -1,6 +1,7 @@
'use client';
import { useEffect, useRef, useState } from 'react';
+import { AlertTriangle } from 'lucide-react';
import { SkinViewer as SkinViewer3D, WalkingAnimation, RunningAnimation, FlyingAnimation, IdleAnimation } from 'skinview3d';
interface SkinViewerProps {
@@ -44,87 +45,102 @@ export default function SkinViewer({
// 预加载皮肤图片以检查是否可访问
useEffect(() => {
- if (!skinUrl) return;
+ if (!skinUrl) {
+ // 使用 requestAnimationFrame 避免同步 setState 触发级联渲染
+ const frame = requestAnimationFrame(() => {
+ setIsLoading(false);
+ setHasError(true);
+ });
+ return () => cancelAnimationFrame(frame);
+ }
- setIsLoading(true);
- setHasError(false);
- setImageLoaded(false);
+ // 使用 requestAnimationFrame 避免同步 setState 触发级联渲染
+ const initFrame = requestAnimationFrame(() => {
+ setIsLoading(true);
+ setHasError(false);
+ setImageLoaded(false);
+ });
+ let isCancelled = false;
const img = new Image();
- img.crossOrigin = 'anonymous'; // 尝试跨域访问
-
+ img.crossOrigin = 'anonymous';
+
img.onload = () => {
+ if (isCancelled) return;
console.log('皮肤图片加载成功:', skinUrl);
setImageLoaded(true);
setIsLoading(false);
- if (onImageLoaded) {
- onImageLoaded();
- }
+ onImageLoaded?.();
};
-
- img.onerror = (error) => {
- console.error('皮肤图片加载失败:', skinUrl, error);
+
+ img.onerror = () => {
+ if (isCancelled) return;
+ console.error('皮肤图片加载失败:', skinUrl);
setHasError(true);
setIsLoading(false);
};
- // 开始加载图片
img.src = skinUrl;
return () => {
- // 清理
+ cancelAnimationFrame(initFrame);
+ isCancelled = true;
img.onload = null;
img.onerror = null;
};
- }, [skinUrl]);
+ }, [skinUrl, onImageLoaded]);
// 初始化3D查看器
useEffect(() => {
if (!canvasRef.current || !imageLoaded || hasError) return;
- try {
- console.log('初始化3D皮肤查看器:', { skinUrl, isSlim, width, height });
-
- // 使用传入的宽高参数初始化
- const canvas = canvasRef.current;
- const viewer = new SkinViewer3D({
- canvas: canvas,
- width: width,
- height: height,
- skin: skinUrl,
- cape: capeUrl,
- model: isSlim ? 'slim' : 'default',
- zoom: 1.0,
- });
+ const canvas = canvasRef.current;
+ let viewer: SkinViewer3D | null = null;
- viewerRef.current = viewer;
+ // 使用 requestAnimationFrame 避免在 effect 体中同步 setState
+ const initFrame = requestAnimationFrame(() => {
+ try {
+ console.log('初始化3D皮肤查看器:', { skinUrl, isSlim, width, height });
- // 设置背景和控制选项
- viewer.background = null; // 透明背景
- viewer.autoRotate = false; // 完全禁用自动旋转
-
- // 外部预览时禁用所有动画和旋转
- if (isExternalPreview) {
- viewer.autoRotate = false;
- viewer.controls.enableRotate = false; // 禁用旋转控制
- viewer.controls.enableZoom = false; // 禁用缩放
- } else {
- viewer.autoRotate = autoRotate && !walking && !running && !jumping;
- viewer.controls.enableRotate = rotation; // 根据参数控制旋转
- viewer.controls.enableZoom = true; // 启用缩放
+ viewer = new SkinViewer3D({
+ canvas: canvas,
+ width: width,
+ height: height,
+ skin: skinUrl,
+ cape: capeUrl,
+ model: isSlim ? 'slim' : 'default',
+ zoom: 1.0,
+ });
+
+ viewerRef.current = viewer;
+
+ // 设置背景和控制选项
+ viewer.background = null; // 透明背景
+ viewer.autoRotate = false; // 完全禁用自动旋转
+
+ // 外部预览时禁用所有动画和旋转
+ if (isExternalPreview) {
+ viewer.autoRotate = false;
+ viewer.controls.enableRotate = false; // 禁用旋转控制
+ viewer.controls.enableZoom = false; // 禁用缩放
+ } else {
+ viewer.autoRotate = autoRotate && !walking && !running && !jumping;
+ viewer.controls.enableRotate = rotation; // 根据参数控制旋转
+ viewer.controls.enableZoom = true; // 启用缩放
+ }
+
+ viewer.controls.enablePan = false; // 禁用平移
+
+ console.log('3D皮肤查看器初始化成功');
+ } catch (error) {
+ console.error('3D皮肤查看器初始化失败:', error);
+ setHasError(true);
}
-
- viewer.controls.enablePan = false; // 禁用平移
-
- console.log('3D皮肤查看器初始化成功');
-
- } catch (error) {
- console.error('3D皮肤查看器初始化失败:', error);
- setHasError(true);
- }
+ });
// 清理函数
return () => {
+ cancelAnimationFrame(initFrame);
if (viewerRef.current) {
try {
viewerRef.current.dispose();
@@ -247,9 +263,11 @@ export default function SkinViewer({
className={`${className} flex items-center justify-center bg-gradient-to-br from-red-50 to-orange-50 dark:from-red-900/20 dark:to-orange-900/20 rounded-xl border-2 border-dashed border-red-300 dark:border-red-700`}
style={{ width: width, height: height }}
>
-
-
⚠️
-
+
+
+
皮肤加载失败
diff --git a/src/components/SliderCaptcha.tsx b/src/components/SliderCaptcha.tsx
index 8c30093..27c6300 100644
--- a/src/components/SliderCaptcha.tsx
+++ b/src/components/SliderCaptcha.tsx
@@ -76,8 +76,8 @@ export const SliderCaptcha: React.FC
= ({ onVerify, onClose
const response = await axios.get(`${API_BASE_URL}/captcha/generate`, {
withCredentials: true // 关键:允许跨域携带凭证
});
- const { code, msg: resMsg, data } = response.data;
- const { masterImage, tileImage, captchaId, y } = data;
+const { code, message: resMsg, data } = response.data;
+const { masterImage, tileImage, captchaId, y } = data;
// 后端返回成功状态(code=200)
if (code === 200) {
@@ -170,7 +170,7 @@ export const SliderCaptcha: React.FC = ({ onVerify, onClose
captchaId: processId // 验证码进程ID
},{ withCredentials: true });
- const { code, msg: resMsg, data } = response.data;
+ const { code, message: resMsg } = response.data;
// 保存后端返回的提示信息
setMsg(resMsg);
@@ -400,10 +400,11 @@ export const SliderCaptcha: React.FC = ({ onVerify, onClose
{/* 背景图片容器:尺寸300x200px,与后端图片尺寸匹配 */}
{backgroundImage && (
+ /* eslint-disable-next-line @next/next/no-img-element */

)}
{/* 可移动拼图块 */}
@@ -411,11 +412,12 @@ export const SliderCaptcha: React.FC
= ({ onVerify, onClose
+ {/* eslint-disable-next-line @next/next/no-img-element */}

{
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 8b9ff69..5d9e50b 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -3,6 +3,7 @@ export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || '/api/v1';
export interface Texture {
id: number;
uploader_id: number;
+ uploader_username?: string;
name: string;
description?: string;
type: 'SKIN' | 'CAPE';
@@ -24,7 +25,6 @@ export interface Profile {
name: string;
skin_id?: number;
cape_id?: number;
- is_active?: boolean;
last_used_at?: string;
created_at: string;
updated_at: string;