diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a8985a9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,41 @@ +# 依赖 +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Next.js 构建产物 +.next +out + +# 测试 +coverage +.nyc_output + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# 操作系统 +.DS_Store +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile +.dockerignore + +# 文档 +docs +*.md + +# 其他 +.env +.env.local +.env.*.local diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml new file mode 100644 index 0000000..fd42b22 --- /dev/null +++ b/.gitea/workflows/docker-build.yml @@ -0,0 +1,87 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + - master + - develop + pull_request: + branches: + - main + - master + workflow_dispatch: + +env: + REGISTRY: code.littlelan.cn + IMAGE_NAME: ${{ github.repository_owner }}/carrotskin + +jobs: + build-and-push: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.GIT_USERNAME }} + password: ${{ secrets.GIT_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # main/master 分支标记为 latest + type=raw,value=latest,enable={{is_default_branch}} + # 所有分支的标签 + type=ref,event=branch + # Git tag 时创建版本标签(如 1.0.0, 1.0) + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + # 每次构建的 SHA 标签 + type=sha + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + provenance: false + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max + + - name: Show image tags + run: | + echo "Built and pushed image with tags:" + echo "${{ steps.meta.outputs.tags }}" + echo "" + echo "Image digest: ${{ steps.meta.outputs.digest }}" + + - name: Summary + if: always() + run: | + echo "## Docker Image Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Image:** ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY + echo "**Tags:**" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY + echo "**Digest:** ${{ steps.meta.outputs.digest }}" >> $GITHUB_STEP_SUMMARY diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..648e604 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# 构建阶段 +FROM node:alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 复制 package 文件 +COPY package*.json ./ + +# 安装所有依赖(包括 devDependencies) +RUN npm ci + +# 复制源代码 +COPY . . + +# 构建应用 +RUN npm run build + +# 生产阶段 +FROM node:alpine AS runner + +# 设置工作目录 +WORKDIR /app + +# 创建非 root 用户 +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# 复制构建产物(standalone 模式) +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +# 设置正确的权限 +RUN chown -R nextjs:nodejs /app + +# 切换到非 root 用户 +USER nextjs + +# 暴露端口 +EXPOSE 3000 + +# 设置环境变量 +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# 启动应用 +CMD ["node", "server.js"] diff --git a/next.config.ts b/next.config.ts index 5f003e5..2408fa2 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: 'standalone', rewrites: async () => { return [ { diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index a03b9fb..e756a94 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -38,6 +38,7 @@ import { generateAvatarUploadUrl, updateAvatarUrl, resetYggdrasilPassword, + deleteTexture, type Texture, type Profile } from '@/lib/api'; @@ -194,7 +195,18 @@ export default function ProfilePage() { const handleDeleteSkin = async (skinId: number) => { if (!confirm('确定要删除这个皮肤吗?')) return; - setMySkins(prev => prev.filter(skin => skin.id !== skinId)); + try { + const response = await deleteTexture(skinId); + if (response.code === 200) { + setMySkins(prev => prev.filter(skin => skin.id !== skinId)); + messageManager.success('皮肤删除成功', { duration: 3000 }); + } else { + messageManager.error(response.message || '删除皮肤失败', { duration: 3000 }); + } + } catch (error) { + console.error('删除皮肤失败:', error); + messageManager.error('删除皮肤失败', { duration: 3000 }); + } }; const handleToggleFavorite = async (skinId: number) => { @@ -373,12 +385,14 @@ export default function ProfilePage() { const handleResetYggdrasilPassword = async () => { if (!confirm('确定要重置Yggdrasil密码吗?这将生成一个新的密码。')) return; - + setIsResettingYggdrasilPassword(true); - + try { const response = await resetYggdrasilPassword(); if (response.code === 200) { + setYggdrasilPassword(response.data.password); + setShowYggdrasilPassword(true); messageManager.success('Yggdrasil密码重置成功!请妥善保管新密码。', { duration: 5000 }); } else { throw new Error(response.message || '重置Yggdrasil密码失败'); @@ -955,14 +969,15 @@ export default function ProfilePage() { } }} > - +
+ +
{skin.name}
diff --git a/src/components/SkinCard.tsx b/src/components/SkinCard.tsx index 9bd84f0..7487826 100644 --- a/src/components/SkinCard.tsx +++ b/src/components/SkinCard.tsx @@ -171,15 +171,15 @@ export default function SkinCard({ {texture.type === 'SKIN' ? ( -
+
setImageLoaded(true)} diff --git a/src/components/SkinDetailModal.tsx b/src/components/SkinDetailModal.tsx index 7858575..d607efd 100644 --- a/src/components/SkinDetailModal.tsx +++ b/src/components/SkinDetailModal.tsx @@ -296,7 +296,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr isSlim={texture.is_slim} width={600} height={600} - className="w-full h-full rounded-2xl shadow-2xl border-2 border-white/60 dark:border-gray-600/60 relative z-10" + className="rounded-2xl shadow-2xl border-2 border-white/60 dark:border-gray-600/60 relative z-10" autoRotate={autoRotate} walking={currentAnimation === 'walking'} running={currentAnimation === 'running'} diff --git a/src/components/SkinViewer.tsx b/src/components/SkinViewer.tsx index 579fbbf..d5b56d6 100644 --- a/src/components/SkinViewer.tsx +++ b/src/components/SkinViewer.tsx @@ -83,28 +83,24 @@ export default function SkinViewer({ 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, + width: width, + height: height, skin: skinUrl, cape: capeUrl, model: isSlim ? 'slim' : 'default', - zoom: 1.0, // 使用blessingskin的zoom方式 + zoom: 1.0, }); viewerRef.current = viewer; - // 设置背景和控制选项 - 参考blessingskin + // 设置背景和控制选项 viewer.background = null; // 透明背景 viewer.autoRotate = false; // 完全禁用自动旋转 - // 调整光照设置,避免皮肤发黑 - viewer.globalLight.intensity = 0.8; // 增加环境光强度 - viewer.cameraLight.intensity = 0.4; // 降低相机光源强度,避免过强的阴影 - // 外部预览时禁用所有动画和旋转 if (isExternalPreview) { viewer.autoRotate = false; @@ -268,11 +264,8 @@ export default function SkinViewer({ ref={canvasRef} className={className} style={{ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - width: '100%', - height: '100%' + width: width, + height: height }} /> ); diff --git a/src/components/SliderCaptcha.tsx b/src/components/SliderCaptcha.tsx index cc9c212..e4947fb 100644 --- a/src/components/SliderCaptcha.tsx +++ b/src/components/SliderCaptcha.tsx @@ -448,10 +448,9 @@ export const SliderCaptcha: React.FC = ({ onVerify, onClose isDragging ? 'scale-110 shadow-xl' : 'scale-100' } ${isVerified || verifyResult === 'error' ? 'cursor-default' : 'cursor-grab active:cursor-grabbing'}`} style={{ left: `${sliderPosition + 2}px`, zIndex: 10 }} - onMouseDown={handleMouseDown} - onTouchStart={handleTouchStart} + onMouseDown={verifyResult === 'error' ? undefined : handleMouseDown} + onTouchStart={verifyResult === 'error' ? undefined : handleTouchStart} ref={sliderRef} - disabled={verifyResult === 'error'} > {getSliderIcon()}
diff --git a/src/components/profile/CharacterCard.tsx b/src/components/profile/CharacterCard.tsx index 0545afe..7add921 100644 --- a/src/components/profile/CharacterCard.tsx +++ b/src/components/profile/CharacterCard.tsx @@ -42,19 +42,32 @@ export default function CharacterCard({ transition={{ duration: 0.2 }} >
- {isEditing ? ( - onEditNameChange(e.target.value)} - className="text-lg font-semibold bg-transparent border-b border-orange-500 focus:outline-none text-gray-900 dark:text-white flex-1 mr-2" - onBlur={() => onSave(profile.uuid)} - onKeyPress={(e) => e.key === 'Enter' && onSave(profile.uuid)} - autoFocus - /> - ) : ( -

{profile.name}

- )} +
+ {isEditing ? ( + onEditNameChange(e.target.value)} + className="text-lg font-semibold bg-transparent border-b border-orange-500 focus:outline-none text-gray-900 dark:text-white flex-1" + onBlur={() => onSave(profile.uuid)} + onKeyPress={(e) => e.key === 'Enter' && onSave(profile.uuid)} + autoFocus + /> + ) : ( + <> +

{profile.name}

+ onEdit(profile.uuid, profile.name)} + className="text-gray-500 hover:text-orange-500 transition-colors" + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.9 }} + title="改名" + > + + + + )} +
{profile.is_active && ( @@ -68,9 +81,8 @@ export default function CharacterCard({ ) : ( @@ -82,32 +94,10 @@ export default function CharacterCard({ )} - - {/* 皮肤选择按钮 */} - onSelectSkin(profile.uuid)} - className="absolute bottom-2 right-2 bg-gradient-to-r from-orange-500 to-amber-500 text-white p-2 rounded-full shadow-lg" - whileHover={{ scale: 1.1 }} - whileTap={{ scale: 0.9 }} - title="选择皮肤" - > - -
{/* 操作按钮 */}
- {!profile.is_active && ( - onSetActive(profile.uuid)} - className="flex-1 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white text-sm py-2 px-3 rounded-lg transition-all duration-200" - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - 使用 - - )} - {isEditing ? ( <> onEdit(profile.uuid, profile.name)} - className="flex-1 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 text-sm py-2 px-3 rounded-lg transition-all duration-200" + onClick={() => onSelectSkin(profile.uuid)} + className="flex-1 bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 text-white text-sm py-2 px-3 rounded-lg transition-all duration-200" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > - - 编辑 + 修改皮肤 onDelete(profile.uuid)} diff --git a/src/components/profile/MySkinsTab.tsx b/src/components/profile/MySkinsTab.tsx index 7027d90..ecafe61 100644 --- a/src/components/profile/MySkinsTab.tsx +++ b/src/components/profile/MySkinsTab.tsx @@ -68,7 +68,6 @@ export default function MySkinsTab({ key={skin.id} texture={skin} onViewDetails={handleViewDetails} - onToggleVisibility={onToggleVisibility} customActions={