forked from CarrotSkin/carrotskin
feat: 完成navbar隐藏优化和侧边栏冻结功能
- 优化navbar滚动隐藏逻辑,更敏感响应 - 添加返回顶部按钮,固定在右下角 - 实现profile页面侧边栏真正冻结效果 - 修复首页滑动指示器位置 - 优化整体布局确保首屏内容完整显示
This commit is contained in:
218
src/components/SkinViewer.tsx
Normal file
218
src/components/SkinViewer.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { SkinViewer as SkinViewer3D, WalkingAnimation, FunctionAnimation } from 'skinview3d';
|
||||
|
||||
interface SkinViewerProps {
|
||||
skinUrl: string;
|
||||
capeUrl?: string;
|
||||
isSlim?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
className?: string;
|
||||
autoRotate?: boolean;
|
||||
walking?: boolean;
|
||||
}
|
||||
|
||||
export default function SkinViewer({
|
||||
skinUrl,
|
||||
capeUrl,
|
||||
isSlim = false,
|
||||
width = 300,
|
||||
height = 300,
|
||||
className = '',
|
||||
autoRotate = true,
|
||||
walking = false,
|
||||
}: SkinViewerProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const viewerRef = useRef<SkinViewer3D | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
||||
// 预加载皮肤图片以检查是否可访问
|
||||
useEffect(() => {
|
||||
if (!skinUrl) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setHasError(false);
|
||||
setImageLoaded(false);
|
||||
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous'; // 尝试跨域访问
|
||||
|
||||
img.onload = () => {
|
||||
console.log('皮肤图片加载成功:', skinUrl);
|
||||
setImageLoaded(true);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
img.onerror = (error) => {
|
||||
console.error('皮肤图片加载失败:', skinUrl, error);
|
||||
setHasError(true);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// 开始加载图片
|
||||
img.src = skinUrl;
|
||||
|
||||
return () => {
|
||||
// 清理
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
};
|
||||
}, [skinUrl]);
|
||||
|
||||
// 初始化3D查看器
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current || !imageLoaded || hasError) return;
|
||||
|
||||
try {
|
||||
console.log('初始化3D皮肤查看器:', { skinUrl, isSlim, width, height });
|
||||
|
||||
// 使用canvas的实际尺寸,参考blessingskin
|
||||
const canvas = canvasRef.current;
|
||||
const viewer = new SkinViewer3D({
|
||||
canvas: canvas,
|
||||
width: canvas.clientWidth || width,
|
||||
height: canvas.clientHeight || height,
|
||||
skin: skinUrl,
|
||||
cape: capeUrl,
|
||||
model: isSlim ? 'slim' : 'default',
|
||||
zoom: 1.0, // 使用blessingskin的zoom方式
|
||||
});
|
||||
|
||||
viewerRef.current = viewer;
|
||||
|
||||
// 设置背景和控制选项 - 参考blessingskin
|
||||
viewer.background = null; // 透明背景
|
||||
viewer.autoRotate = false; // 禁用自动旋转
|
||||
|
||||
// 禁用所有交互控制
|
||||
viewer.controls.enableRotate = false; // 禁用旋转控制
|
||||
viewer.controls.enableZoom = false; // 禁用缩放
|
||||
viewer.controls.enablePan = false; // 禁用平移
|
||||
|
||||
console.log('3D皮肤查看器初始化成功');
|
||||
|
||||
} catch (error) {
|
||||
console.error('3D皮肤查看器初始化失败:', error);
|
||||
setHasError(true);
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
if (viewerRef.current) {
|
||||
try {
|
||||
viewerRef.current.dispose();
|
||||
viewerRef.current = null;
|
||||
console.log('3D皮肤查看器已清理');
|
||||
} catch (error) {
|
||||
console.error('清理3D皮肤查看器失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [skinUrl, capeUrl, isSlim, width, height, autoRotate, walking, imageLoaded, hasError]);
|
||||
|
||||
// 当皮肤URL改变时更新
|
||||
useEffect(() => {
|
||||
if (viewerRef.current && skinUrl && imageLoaded) {
|
||||
try {
|
||||
console.log('更新皮肤URL:', skinUrl);
|
||||
viewerRef.current.loadSkin(skinUrl);
|
||||
} catch (error) {
|
||||
console.error('更新皮肤失败:', error);
|
||||
}
|
||||
}
|
||||
}, [skinUrl, imageLoaded]);
|
||||
|
||||
// 监听容器尺寸变化并调整viewer大小 - 参考blessingskin
|
||||
useEffect(() => {
|
||||
if (viewerRef.current && canvasRef.current) {
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
viewerRef.current.setSize(rect.width, rect.height);
|
||||
}
|
||||
}, [imageLoaded]); // 当图片加载完成后调整尺寸
|
||||
|
||||
// 当披风URL改变时更新
|
||||
useEffect(() => {
|
||||
if (viewerRef.current && capeUrl && imageLoaded) {
|
||||
try {
|
||||
console.log('更新披风URL:', capeUrl);
|
||||
viewerRef.current.loadCape(capeUrl);
|
||||
} catch (error) {
|
||||
console.error('更新披风失败:', error);
|
||||
}
|
||||
} else if (viewerRef.current && !capeUrl && imageLoaded) {
|
||||
try {
|
||||
viewerRef.current.loadCape(null);
|
||||
} catch (error) {
|
||||
console.error('移除披风失败:', error);
|
||||
}
|
||||
}
|
||||
}, [capeUrl, imageLoaded]);
|
||||
|
||||
// 当模型类型改变时更新
|
||||
useEffect(() => {
|
||||
if (viewerRef.current && skinUrl && imageLoaded) {
|
||||
try {
|
||||
console.log('更新模型类型:', isSlim ? 'slim' : 'default');
|
||||
viewerRef.current.loadSkin(skinUrl, { model: isSlim ? 'slim' : 'default' });
|
||||
} catch (error) {
|
||||
console.error('更新模型失败:', error);
|
||||
}
|
||||
}
|
||||
}, [isSlim, skinUrl, imageLoaded]);
|
||||
|
||||
// 错误状态显示
|
||||
if (hasError) {
|
||||
return (
|
||||
<div
|
||||
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 }}
|
||||
>
|
||||
<div className="text-center p-4">
|
||||
<div className="text-4xl mb-2">⚠️</div>
|
||||
<div className="text-sm text-red-600 dark:text-red-400 font-medium mb-1">
|
||||
皮肤加载失败
|
||||
</div>
|
||||
<div className="text-xs text-red-500 dark:text-red-500">
|
||||
图片链接可能无效或存在访问限制
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 加载状态显示
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className={`${className} flex items-center justify-center bg-gradient-to-br from-orange-50 to-amber-50 dark:from-gray-700 dark:to-gray-600 rounded-xl`}
|
||||
style={{ width: width, height: height }}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="animate-spin w-8 h-8 border-3 border-orange-500 border-t-transparent rounded-full mx-auto mb-3"></div>
|
||||
<div className="text-sm text-orange-600 dark:text-orange-400 font-medium">
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={className}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user