28 Commits

Author SHA1 Message Date
lan
fdd1d0c17b fix: 修复ci缓存导致的错误 2026-02-24 17:07:48 +08:00
lan
42c2fb4ce3 fix: 修复ci中出现的错误 2026-02-24 12:50:38 +08:00
lan
2e85be4657 chore: remove unnecessary QEMU setup for single platform build 2026-02-24 11:27:29 +08:00
lan
dad28881ed chore: add Docker configuration and Gitea CI/CD workflow 2026-02-24 11:26:38 +08:00
lan
0c6c0ae1ac fix: 修复了大写导致的不能正常上传镜像 2026-02-24 11:18:00 +08:00
lan
2124790c8d feat: add Docker support and texture deletion functionality
Add Docker configuration with standalone output mode for containerized
deployment. Implement texture deletion API with proper error handling
and user feedback. Fix skin viewer sizing issues by using explicit
dimensions and removing conflicting layout properties. Add captcha
ID parameter to registration flow. Improve profile page UX including
Yggdrasil password reset display and character card editing controls.
2026-02-24 11:09:37 +08:00
f5455afaf2 Merge pull request '解决了不跟手问题' (#8) from feature/slide-captcha into main
Reviewed-on: CarrotSkin/carrotskin#8
2026-02-15 00:26:04 +08:00
eed6920d4a 移除了滑动验证码的延迟渲染 2026-02-14 00:11:00 +08:00
00984b6d67 移除了滑动验证码的延迟渲染 2026-02-13 23:59:56 +08:00
344cae80af Merge pull request '添加了滑动验证码组件' (#7) from feature/slide-captcha into main
Reviewed-on: CarrotSkin/carrotskin#7
2026-02-12 23:08:56 +08:00
321b32e312 添加了滑动验证码组件 2026-02-12 20:54:06 +08:00
914ea7524b Merge pull request 'WIP:合并滑动验证码于注册' (#6) from feature/slide-captcha into main
Reviewed-on: CarrotSkin/carrotskin#6
2026-01-22 21:23:49 +08:00
33342d1a85 集成了滑动验证码组件于注册界面 2026-01-22 20:52:08 +08:00
acf04cad9b 添加了滑动验证码组件 2026-01-22 20:44:19 +08:00
LostSalt
87a8042b77 添加了滑动验证码组件 2026-01-20 01:18:26 +08:00
LostSalt
f3dc7cda21 添加了滑动验证码组件 2026-01-20 01:13:56 +08:00
4d3f34e1c8 Merge pull request '移除神必谷歌字体' (#5) from uNagi/carrotskin:main into main
Reviewed-on: CarrotSkin/carrotskin#5
2026-01-19 21:23:40 +08:00
Mikuisnotavailable
d830b73770 移除神必谷歌字体 2026-01-19 20:08:54 +08:00
6b7a057cb4 修复:主界面下拉出现两个重复的回到页面顶部按钮。注释了NavBar.tsx中的按钮,保留并修改了scrollToTop.tsx中的按钮样式。 2026-01-13 20:32:13 +08:00
f1e8437d31 更新了package-lock.json(npm自动更新) 2026-01-13 17:49:22 +08:00
0c7a54fb1f 修改了css文件头的过时导包语法,在gitignore中添加排除vscode工作区文件 2026-01-13 17:23:04 +08:00
fba619d884 Merge pull request 'main' (#4) from uNagi/carrotskin:main into main
Reviewed-on: CarrotSkin/carrotskin#4
2026-01-11 18:54:00 +08:00
d7e627a8db Merge branch 'main' into main 2026-01-11 18:53:11 +08:00
Mikuisnotavailable
70c541d57c 修复:移除鼠标跟随元素的防抖处理,因为我发现抖动是因为我这边性能不行( 2026-01-10 16:40:37 +08:00
Mikuisnotavailable
ed83326cdc 升级eslint-config-next版本到16.1.1,保持版本一致性 2026-01-10 10:22:59 +08:00
Mikuisnotavailable
e41a66f176 Merge branch 'main' of https://code.littlelan.cn/uNagi/carrotskin 2026-01-10 10:10:11 +08:00
Mikuisnotavailable
f84e5e4d24 修复:升级React和Next.js版本修复CVE-2025-55182漏洞 2026-01-10 10:09:45 +08:00
lafay
fe3f324b22 refactor: enhance SkinCard and SkinViewer components for improved visuals
- Updated background gradient colors in SkinCard for better contrast.
- Wrapped SkinViewer in a div for consistent styling and layout.
- Adjusted overlay gradient opacity in SkinCard for a subtler effect.
- Increased global light intensity and adjusted camera light in SkinViewer to enhance skin visibility and reduce harsh shadows.
2026-01-10 02:55:43 +08:00
30 changed files with 1122 additions and 246 deletions

41
.dockerignore Normal file
View File

@@ -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

View File

@@ -0,0 +1,85 @@
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: carrotskin/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.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- 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 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
# 禁用 buildcache 以避免 413 错误
# 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

3
.gitignore vendored
View File

@@ -1,5 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# VS Code Workspace File
*.code-workspace
# dependencies
/node_modules
/.pnp

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/carrotskin.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,9 @@
<component name="libraryTable">
<library name="MPLUSRounded1c-Regular.typeface.json">
<CLASSES>
<root url="jar://$PROJECT_DIR$/node_modules/three/examples/fonts/MPLUSRounded1c/MPLUSRounded1c-Regular.typeface.json.zip!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

6
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="openjdk-24" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/carrotskin.iml" filepath="$PROJECT_DIR$/.idea/carrotskin.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

49
Dockerfile Normal file
View File

@@ -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"]

View File

@@ -1,7 +1,15 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: 'standalone',
rewrites: async () => {
return [
{
source: '/api/v1/:path*',
destination: 'http://localhost:8080/api/v1/:path*',
},
];
},
};
export default nextConfig;

238
package-lock.json generated
View File

@@ -13,13 +13,14 @@
"@heroicons/react": "^2.2.0",
"@prisma/client": "^7.1.0",
"@types/three": "^0.181.0",
"axios": "^1.13.2",
"framer-motion": "^12.23.25",
"lucide-react": "^0.555.0",
"next": "16.0.7",
"next": "^16.1.1",
"next-auth": "^4.24.13",
"prisma": "^7.1.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"skinview3d": "^3.4.1",
"three": "^0.181.2"
},
@@ -29,7 +30,7 @@
"@types/react": "^19.2.7",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.7",
"eslint-config-next": "^16.1.1",
"tailwindcss": "^4",
"typescript": "^5"
}
@@ -1296,15 +1297,15 @@
}
},
"node_modules/@next/env": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/@next/env/-/env-16.0.7.tgz",
"integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/@next/env/-/env-16.1.1.tgz",
"integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.7.tgz",
"integrity": "sha512-hFrTNZcMEG+k7qxVxZJq3F32Kms130FAhG8lvw2zkKBgAcNOJIxlljNiCjGygvBshvaGBdf88q2CqWtnqezDHA==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.1.tgz",
"integrity": "sha512-Ovb/6TuLKbE1UiPcg0p39Ke3puyTCIKN9hGbNItmpQsp+WX3qrjO3WaMVSi6JHr9X1NrmthqIguVHodMJbh/dw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1312,9 +1313,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz",
"integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz",
"integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==",
"cpu": [
"arm64"
],
@@ -1328,9 +1329,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz",
"integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz",
"integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==",
"cpu": [
"x64"
],
@@ -1344,9 +1345,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz",
"integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz",
"integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==",
"cpu": [
"arm64"
],
@@ -1360,9 +1361,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz",
"integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz",
"integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==",
"cpu": [
"arm64"
],
@@ -1376,9 +1377,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz",
"integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz",
"integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==",
"cpu": [
"x64"
],
@@ -1392,9 +1393,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz",
"integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz",
"integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==",
"cpu": [
"x64"
],
@@ -1408,9 +1409,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz",
"integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz",
"integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==",
"cpu": [
"arm64"
],
@@ -1424,9 +1425,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz",
"integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz",
"integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==",
"cpu": [
"x64"
],
@@ -2955,6 +2956,12 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2990,6 +2997,17 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3011,7 +3029,6 @@
"version": "2.9.0",
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.0.tgz",
"integrity": "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -3126,7 +3143,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -3273,6 +3289,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
@@ -3468,6 +3496,15 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz",
@@ -3522,7 +3559,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -3653,7 +3689,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3663,7 +3698,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3701,7 +3735,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -3714,7 +3747,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -3841,13 +3873,13 @@
}
},
"node_modules/eslint-config-next": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-16.0.7.tgz",
"integrity": "sha512-WubFGLFHfk2KivkdRGfx6cGSFhaQqhERRfyO8BRx+qiGPGp7WLKcPvYC4mdx1z3VhVRcrfFzczjjTrbJZOpnEQ==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-16.1.1.tgz",
"integrity": "sha512-55nTpVWm3qeuxoQKLOjQVciKZJUphKrNM0fCcQHAIOGl6VFXgaqeMfv0aKJhs7QtcnlAPhNVqsqRfRjeKBPIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/eslint-plugin-next": "16.0.7",
"@next/eslint-plugin-next": "16.1.1",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.32.0",
@@ -4284,9 +4316,9 @@
"license": "MIT"
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"version": "1.20.1",
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -4363,6 +4395,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz",
@@ -4395,6 +4447,22 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/framer-motion": {
"version": "12.23.25",
"resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.23.25.tgz",
@@ -4426,7 +4494,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4496,7 +4563,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -4527,7 +4593,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -4632,7 +4697,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4716,7 +4780,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4729,7 +4792,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -4745,7 +4807,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -5817,7 +5878,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -5853,6 +5913,27 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
@@ -5981,13 +6062,14 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.0.7",
"resolved": "https://registry.npmmirror.com/next/-/next-16.0.7.tgz",
"integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==",
"version": "16.1.1",
"resolved": "https://registry.npmmirror.com/next/-/next-16.1.1.tgz",
"integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
"license": "MIT",
"dependencies": {
"@next/env": "16.0.7",
"@next/env": "16.1.1",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -5999,14 +6081,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.0.7",
"@next/swc-darwin-x64": "16.0.7",
"@next/swc-linux-arm64-gnu": "16.0.7",
"@next/swc-linux-arm64-musl": "16.0.7",
"@next/swc-linux-x64-gnu": "16.0.7",
"@next/swc-linux-x64-musl": "16.0.7",
"@next/swc-win32-arm64-msvc": "16.0.7",
"@next/swc-win32-x64-msvc": "16.0.7",
"@next/swc-darwin-arm64": "16.1.1",
"@next/swc-darwin-x64": "16.1.1",
"@next/swc-linux-arm64-gnu": "16.1.1",
"@next/swc-linux-arm64-musl": "16.1.1",
"@next/swc-linux-x64-gnu": "16.1.1",
"@next/swc-linux-x64-musl": "16.1.1",
"@next/swc-win32-arm64-msvc": "16.1.1",
"@next/swc-win32-x64-msvc": "16.1.1",
"sharp": "^0.34.4"
},
"peerDependencies": {
@@ -6611,6 +6693,12 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
@@ -6669,24 +6757,24 @@
}
},
"node_modules/react": {
"version": "19.2.0",
"resolved": "https://registry.npmmirror.com/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"version": "19.2.3",
"resolved": "https://registry.npmmirror.com/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.0",
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"version": "19.2.3",
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.0"
"react": "^19.2.3"
}
},
"node_modules/react-is": {

View File

@@ -14,13 +14,14 @@
"@heroicons/react": "^2.2.0",
"@prisma/client": "^7.1.0",
"@types/three": "^0.181.0",
"axios": "^1.13.2",
"framer-motion": "^12.23.25",
"lucide-react": "^0.555.0",
"next": "16.0.7",
"next": "^16.1.1",
"next-auth": "^4.24.13",
"prisma": "^7.1.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"skinview3d": "^3.4.1",
"three": "^0.181.2"
},
@@ -30,7 +31,7 @@
"@types/react": "^19.2.7",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.7",
"eslint-config-next": "^16.1.1",
"tailwindcss": "^4",
"typescript": "^5"
}

View File

@@ -7,6 +7,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { EyeIcon, EyeSlashIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline';
import { useAuth } from '@/contexts/AuthContext';
import { errorManager } from '@/components/ErrorNotification';
import SliderCaptcha from '@/components/SliderCaptcha';
export default function AuthPage() {
const [isLoginMode, setIsLoginMode] = useState(true);
@@ -27,6 +28,9 @@ export default function AuthPage() {
const [authError, setAuthError] = useState('');
const [isSendingCode, setIsSendingCode] = useState(false);
const [codeTimer, setCodeTimer] = useState(0);
const [showCaptcha, setShowCaptcha] = useState(false);
const [isCaptchaVerified, setIsCaptchaVerified] = useState(false);
const [captchaId, setCaptchaId] = useState<string | undefined>();
const { login, register } = useAuth();
const router = useRouter();
@@ -161,6 +165,39 @@ export default function AuthPage() {
}
};
const handleCaptchaVerify = (success: boolean) => {
if (success) {
setIsCaptchaVerified(true);
setShowCaptcha(false);
// 验证码验证成功后,继续注册流程
handleRegisterAfterCaptcha();
} else {
setIsCaptchaVerified(false);
setShowCaptcha(false);
errorManager.showError('验证码验证失败,请重试');
}
};
const handleRegisterAfterCaptcha = async () => {
setIsLoading(true);
setAuthError('');
try {
await register(formData.username, formData.email, formData.password, formData.verificationCode, captchaId);
errorManager.showSuccess('注册成功欢迎加入CarrotSkin');
router.push('/');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '注册失败,请稍后重试';
setAuthError(errorMessage);
errorManager.showError(errorMessage);
// 注册失败时重置验证码状态
setIsCaptchaVerified(false);
} finally {
setIsLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -192,21 +229,14 @@ export default function AuthPage() {
} else {
if (!validateRegisterForm()) return;
setIsLoading(true);
setAuthError('');
try {
await register(formData.username, formData.email, formData.password, formData.verificationCode);
errorManager.showSuccess('注册成功欢迎加入CarrotSkin');
router.push('/');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '注册失败,请稍后重试';
setAuthError(errorMessage);
errorManager.showError(errorMessage);
} finally {
setIsLoading(false);
// 如果验证码还未验证,显示滑动验证码
if (!isCaptchaVerified) {
setShowCaptcha(true);
return;
}
// 如果验证码已验证,直接进行注册
handleRegisterAfterCaptcha();
}
};
@@ -727,6 +757,15 @@ export default function AuthPage() {
</motion.div>
</motion.div>
</div>
{/* Slider Captcha Component */}
{showCaptcha && (
<SliderCaptcha
onVerify={handleCaptchaVerify}
onClose={() => setShowCaptcha(false)}
/>
)}
</div>
);
}
}

View File

@@ -1,13 +1,13 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@import "tailwindcss";
@tailwind base;
/* @tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind utilities; */
:root {
--background: #ffffff;
--foreground: #171717;
--navbar-height: 64px; /* 与pt-16对应 */
--navbar-height: 64px;
/* 与pt-16对应 */
--primary-orange: #f97316;
--primary-orange-dark: #ea580c;
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
@@ -25,7 +25,7 @@
body {
color: var(--foreground);
background: var(--background);
font-family: 'Inter', Arial, Helvetica, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
scroll-behavior: smooth;
}
@@ -132,7 +132,7 @@ body {
background-color: #1f2937;
border-color: #c2410c;
}
.card-minecraft:hover {
border-color: var(--primary-orange);
}
@@ -159,96 +159,97 @@ body {
/* 现代布局解决方案 */
@layer utilities {
/* 全屏减去navbar高度 */
.h-screen-nav {
height: calc(100vh - var(--navbar-height));
}
/* 侧栏最大高度,确保底部按钮可见 */
.sidebar-max-height {
max-height: calc(100vh - var(--navbar-height) - 120px);
}
/* 首页hero section专用高度 */
.min-h-screen-nav {
min-height: calc(100vh - var(--navbar-height));
}
/* 增强的过渡效果 */
.transition-all-enhanced {
transition: all var(--transition-normal);
}
.transition-colors-enhanced {
transition: color var(--transition-normal), background-color var(--transition-normal), border-color var(--transition-normal);
}
.transition-transform-enhanced {
transition: transform var(--transition-normal);
}
/* 微交互效果 */
.micro-interaction {
transition: all var(--transition-fast);
}
.micro-interaction:hover {
transform: scale(1.02);
}
.micro-interaction:active {
transform: scale(0.98);
}
/* 加载动画 */
.animate-pulse-slow {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.animate-pulse-fast {
animation: pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* 弹跳动画 */
.animate-bounce-slow {
animation: bounce 2s infinite;
}
.animate-bounce-fast {
animation: bounce 1s infinite;
}
/* 旋转动画 */
.animate-spin-slow {
animation: spin 3s linear infinite;
}
.animate-spin-fast {
animation: spin 1s linear infinite;
}
/* 渐变动画 */
.animate-gradient {
background-size: 200% 200%;
animation: gradient 3s ease infinite;
}
/* 阴影动画 */
.shadow-animated {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: box-shadow var(--transition-normal);
}
.shadow-animated:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* 模糊动画 */
.backdrop-blur-animated {
backdrop-filter: blur(8px);
transition: backdrop-filter var(--transition-normal);
}
.backdrop-blur-animated:hover {
backdrop-filter: blur(16px);
}
@@ -259,18 +260,23 @@ body {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes float {
0%, 100% {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
@@ -280,6 +286,7 @@ body {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
@@ -290,6 +297,7 @@ body {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
@@ -301,6 +309,7 @@ body {
transform: translateY(-30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
@@ -312,6 +321,7 @@ body {
transform: translateX(-30px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
@@ -323,6 +333,7 @@ body {
transform: translateX(30px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
@@ -334,6 +345,7 @@ body {
transform: scale(0.9);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
@@ -345,6 +357,7 @@ body {
transform: scale(1);
opacity: 1;
}
to {
transform: scale(0.9);
opacity: 0;
@@ -378,23 +391,19 @@ body {
/* 加载状态样式 */
.loading-shimmer {
background: linear-gradient(
90deg,
#f0f0f0 0%,
#e0e0e0 50%,
#f0f0f0 100%
);
background: linear-gradient(90deg,
#f0f0f0 0%,
#e0e0e0 50%,
#f0f0f0 100%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.dark .loading-shimmer {
background: linear-gradient(
90deg,
#374151 0%,
#4b5563 50%,
#374151 100%
);
background: linear-gradient(90deg,
#374151 0%,
#4b5563 50%,
#374151 100%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@@ -426,7 +435,10 @@ body {
/* 响应式动效 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
@@ -435,15 +447,16 @@ body {
/* 触摸设备优化 */
@media (hover: none) and (pointer: coarse) {
.btn-carrot:hover,
.btn-carrot-outline:hover,
.card-minecraft:hover {
transform: none;
}
.btn-carrot:active,
.btn-carrot-outline:active,
.card-minecraft:active {
transform: scale(0.98);
}
}
}

View File

@@ -1,5 +1,4 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Navbar from "@/components/Navbar";
import { AuthProvider } from "@/contexts/AuthContext";
@@ -9,12 +8,6 @@ import { ErrorNotificationContainer } from "@/components/ErrorNotification";
import ScrollToTop from "@/components/ScrollToTop";
import PageTransition from "@/components/PageTransition";
const inter = Inter({
subsets: ["latin"],
weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
display: 'swap',
});
export const metadata: Metadata = {
title: "CarrotSkin - 现代化Minecraft Yggdrasil皮肤站",
description: "新一代Minecraft Yggdrasil皮肤站为创作者打造的现代化皮肤管理平台",
@@ -34,7 +27,7 @@ export default function RootLayout({
}>) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<body>
<AuthProvider>
<Navbar />
<PageTransition>

View File

@@ -23,22 +23,15 @@ export default function Home() {
const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
let timeoutId: number | undefined;
const handleMouseMove = (e: MouseEvent) => {
clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
setMousePosition({ x: e.clientX, y: e.clientY });
}, 16);
setMousePosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, []);

View File

@@ -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() {
}
}}
>
<SkinViewer
skinUrl={skin.url}
isSlim={skin.is_slim}
width={200}
height={200}
className="w-full h-full"
autoRotate={false}
/>
<div className="w-full h-full flex items-center justify-center">
<SkinViewer
skinUrl={skin.url}
isSlim={skin.is_slim}
width={180}
height={180}
autoRotate={false}
/>
</div>
<div className="absolute bottom-0 left-0 right-0 bg-black/50 text-white text-xs p-2 text-center">
{skin.name}
</div>

View File

@@ -464,7 +464,7 @@ export default function Navbar() {
</motion.nav>
{/* 返回顶部按钮 */}
<AnimatePresence>
{/* <AnimatePresence>
{showScrollTop && (
<motion.button
initial={{ opacity: 0, scale: 0.8, y: 20 }}
@@ -485,7 +485,7 @@ export default function Navbar() {
</motion.div>
</motion.button>
)}
</AnimatePresence>
</AnimatePresence> */}
</>
);
}

View File

@@ -2,14 +2,15 @@
import { motion, AnimatePresence } from 'framer-motion';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useState, useRef } from 'react';
import { useEffect, useState, useRef, Suspense } from 'react';
import { useRouter } from 'next/navigation';
interface PageTransitionProps {
children: React.ReactNode;
}
export default function PageTransition({ children }: PageTransitionProps) {
// 内部组件:使用 useSearchParams 的部分
function PageTransitionContent({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
@@ -96,13 +97,13 @@ export default function PageTransition({ children }: PageTransitionProps) {
};
const getLoadingVariants = () => ({
initial: {
initial: {
opacity: 0,
scale: 0.8,
y: 20
},
animate: {
opacity: 1,
animate: {
opacity: 1,
scale: 1,
y: 0,
transition: {
@@ -133,17 +134,17 @@ export default function PageTransition({ children }: PageTransitionProps) {
>
<div className="text-center">
<motion.div
animate={{
animate={{
rotate: 360,
scale: [1, 1.1, 1]
}}
transition={{
transition={{
rotate: { duration: 1, repeat: Infinity },
scale: { duration: 1.5, repeat: Infinity }
}}
className="w-12 h-12 border-4 border-orange-500 border-t-transparent rounded-full mx-auto mb-4"
/>
<motion.p
<motion.p
className="text-lg font-medium text-gray-700 dark:text-gray-300"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
@@ -171,3 +172,20 @@ export default function PageTransition({ children }: PageTransitionProps) {
</>
);
}
// 加载状态组件
function PageTransitionFallback() {
return (
<div className="min-h-screen">
{null}
</div>
);
}
export default function PageTransition({ children }: PageTransitionProps) {
return (
<Suspense fallback={<PageTransitionFallback />}>
<PageTransitionContent>{children}</PageTransitionContent>
</Suspense>
);
}

View File

@@ -41,7 +41,7 @@ export default function ScrollToTop() {
exit={{ opacity: 0, scale: 0.8, y: 20 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
onClick={scrollToTop}
className="fixed bottom-6 right-6 w-12 h-12 bg-gradient-to-br from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center z-40 group"
className="fixed bottom-6 right-6 w-12 h-12 bg-gradient-to-br from-orange-400/70 to-amber-300/70 hover:from-orange-600/70 hover:to-orange-700/70 text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center z-40 group"
whileHover={{ scale: 1.1, y: -2 }}
whileTap={{ scale: 0.9 }}
>

View File

@@ -146,7 +146,7 @@ export default function SkinCard({
}}
>
{/* 3D预览区域 */}
<div className="relative aspect-square bg-gradient-to-br from-orange-50 to-amber-50 dark:from-gray-700 dark:to-gray-600 overflow-hidden">
<div className="relative aspect-square bg-gradient-to-br from-orange-100 to-amber-100 dark:from-gray-600 dark:to-gray-500 overflow-hidden">
{/* 加载状态 */}
<AnimatePresence>
{!imageLoaded && (
@@ -171,18 +171,20 @@ export default function SkinCard({
</AnimatePresence>
{texture.type === 'SKIN' ? (
<SkinViewer
skinUrl={texture.url}
isSlim={texture.is_slim}
width={300}
height={300}
className={`w-full h-full transition-all duration-500 ${
imageLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
} ${isHovered ? 'scale-110' : ''}`}
autoRotate={isHovered}
walking={false}
onImageLoaded={() => setImageLoaded(true)}
/>
<div className="relative w-full h-full flex items-center justify-center bg-white dark:bg-gray-800">
<SkinViewer
skinUrl={texture.url}
isSlim={texture.is_slim}
width={280}
height={280}
className={`transition-opacity duration-500 ${
imageLoaded ? 'opacity-100' : 'opacity-0'
}`}
autoRotate={isHovered}
walking={false}
onImageLoaded={() => setImageLoaded(true)}
/>
</div>
) : (
<div className="absolute inset-0 flex items-center justify-center">
<motion.div
@@ -208,7 +210,7 @@ export default function SkinCard({
initial={{ opacity: 0 }}
animate={{ opacity: isHovered ? 1 : 0 }}
transition={{ duration: 0.3 }}
className="absolute inset-0 bg-gradient-to-br from-black/40 via-black/30 to-transparent flex items-center justify-center"
className="absolute inset-0 bg-gradient-to-br from-black/10 via-black/5 to-transparent flex items-center justify-center"
>
<div className="flex gap-3">
<motion.button

View File

@@ -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'}

View File

@@ -83,21 +83,21 @@ 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; // 完全禁用自动旋转
@@ -264,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
}}
/>
);

View File

@@ -0,0 +1,486 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Shield, X, Check } from 'lucide-react';
import axios from 'axios';
import { API_BASE_URL } from '@/lib/api';
/**
* 滑块验证码组件属性接口定义
* @interface SliderCaptchaProps
* @property {function} onVerify - 验证结果回调函数,参数为验证是否成功
* @property {function} onClose - 关闭验证码组件的回调函数
*/
interface SliderCaptchaProps {
onVerify: (success: boolean) => void;
onClose: () => void;
}
// 轨道宽度(与背景图宽度一致)
const TRACK_WIDTH = 300;
// 滑块按钮宽度
const SLIDER_WIDTH = 50;
// 背景图宽度(与后端返回的背景图尺寸匹配)
// const CANVAS_WIDTH = 300;
/**
* 滑块验证码组件
* 功能:通过拖拽滑块完成拼图验证,与后端交互获取验证码资源和验证结果
* 特点:
* - 支持鼠标和触摸事件适配PC和移动端
* - 与后端接口交互,获取背景图、拼图和验证结果
* - 包含验证状态反馈和错误处理
* @param {SliderCaptchaProps} props - 组件属性
* @returns {JSX.Element} 滑块验证码组件JSX元素
*/
export const SliderCaptcha: React.FC<SliderCaptchaProps> = ({ onVerify, onClose }) => {
// 拖拽状态:是否正在拖拽滑块
const [isDragging, setIsDragging] = useState(false);
// 滑块当前位置x坐标
const [sliderPosition, setSliderPosition] = useState(0);
// 拼图y坐标从后端获取
const [puzzleY, setPuzzleY] = useState(0);
// 验证状态:是否验证成功
const [isVerified, setIsVerified] = useState(false);
// 加载状态:是否正在加载资源或验证中
const [isLoading, setIsLoading] = useState(false);
// 尝试次数:记录验证失败的次数
const [attempts, setAttempts] = useState(0);
// 错误显示状态:是否显示验证错误提示
const [showError, setShowError] = useState(false);
// 拖拽偏移量:鼠标/触摸点与滑块中心的偏移,用于精准计算滑块位置
const [dragOffset, setDragOffset] = useState(0);
// 背景图Base64字符串从后端获取
const [backgroundImage, setBackgroundImage] = useState<string>('');
// 拼图Base64字符串从后端获取
const [puzzleImage, setPuzzleImage] = useState<string>('');
// 验证码进程ID从后端获取用于验证时标识当前验证码
const [processId, setProcessId] = useState<string>('');
// 验证结果false-未验证/验证失败true-验证成功,'error'-请求错误
const [verifyResult, setVerifyResult] = useState<boolean | string>(false);
// 提示信息:显示后端返回的提示或默认提示
const [msg, setMsg] = useState<string>('拖动滑块完成拼图');
const sliderRef = useRef<HTMLDivElement | null>(null);
const trackRef = useRef<HTMLDivElement | null>(null);
/**
* 获取验证码资源(背景图、拼图、位置信息等)
* 从后端接口请求验证码所需的资源数据包括背景图、拼图的Base64编码
* 拼图的y坐标和进程ID并初始化拼图的x坐标
*/
const fetchCaptchaResources = useCallback(async () => {
try {
// 开始加载,设置加载状态
setIsLoading(true);
// 请求验证码资源接口
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;
// 后端返回成功状态code=200
if (code === 200) {
// 设置背景图
setBackgroundImage(masterImage);
// 设置拼图图片
setPuzzleImage(tileImage);
// 设置拼图y坐标从后端获取以背景图左上角为原点
setPuzzleY(y);
// 设置进程ID用于后续验证
setProcessId(captchaId);
// 随机生成拼图x坐标确保拼图在背景图内
// setPuzzlePosition(Math.random() * (CANVAS_WIDTH - 50 - 50) + 50);
// 保存后端返回的提示信息
setMsg(resMsg);
// 结束加载状态
setIsLoading(false);
return;
}
// 后端返回失败状态非200
setMsg(resMsg || '生成验证码失败');
setVerifyResult('error');
setIsLoading(false);
} catch (error) {
// 捕获请求异常
const errMsg = '获取验证码资源失败: ' + (error as Error).message;
console.error(errMsg);
setMsg(errMsg);
setVerifyResult('error');
setIsLoading(false);
}
}, []);
/**
* 组件挂载时自动获取验证码资源
* 依赖fetchCaptchaResources函数确保函数变化时重新执行
*/
useEffect(() => {
fetchCaptchaResources();
}, [fetchCaptchaResources]);
/**
* 开始拖拽处理函数
* 记录初始拖拽位置和偏移量,设置拖拽状态
* @param {number} clientX - 鼠标/触摸点的x坐标
*/
const handleStart = useCallback((clientX: number) => {
if (isVerified || isLoading || verifyResult === 'error') return;
setIsDragging(true);
setShowError(false);
const slider = sliderRef.current;
if (slider) {
const rect = slider.getBoundingClientRect();
setDragOffset(clientX - rect.left - SLIDER_WIDTH / 2);
}
}, [isVerified, isLoading, verifyResult]);
/**
* 拖拽移动处理函数
* 根据鼠标/触摸点的移动更新滑块位置,限制滑块在轨道范围内
* @param {number} clientX - 鼠标/触摸点的x坐标
*/
const handleMove = useCallback((clientX: number) => {
if (!isDragging || isVerified || isLoading || verifyResult === 'error') return;
const track = trackRef.current;
if (!track) return;
const rect = track.getBoundingClientRect();
const x = clientX - rect.left - dragOffset;
const maxPosition = TRACK_WIDTH - SLIDER_WIDTH;
const newPosition = Math.max(0, Math.min(x, maxPosition));
setSliderPosition(newPosition);
}, [isDragging, isVerified, isLoading, dragOffset, verifyResult]);
/**
* 结束拖拽处理函数
* 拖拽结束后向后端发送验证请求,处理验证结果
*/
const handleEnd = useCallback(async () => {
if (!isDragging || isVerified || isLoading || verifyResult === 'error') return;
setIsDragging(false);
setIsLoading(true);
try {
// 向后端发送验证请求参数为滑块位置x坐标和进程ID
// 使用sliderPosition作为dx值这是拼图块左上角的位置
const response = await axios.post(`${API_BASE_URL}/captcha/verify`, {
dx: sliderPosition, // 滑块位置拼图左上角x坐标以背景图左上角为原点
captchaId: processId // 验证码进程ID
},{ withCredentials: true });
const { code, msg: resMsg, data } = response.data;
// 保存后端返回的提示信息
setMsg(resMsg);
// 根据后端返回的code判断验证结果
// 验证成功code=200
if (code === 200) {
// 增加尝试次数
setAttempts(prev => prev + 1);
// 重置所有状态,确保验证成功状态的纯净性
setShowError(false);
setVerifyResult(false);
// 直接设置验证成功状态,不使用异步更新
setIsVerified(true);
// 延迟1.2秒后调用验证成功回调
setTimeout(() => onVerify(true), 1200);
}
// 验证失败code=400
else if (code === 400) {
// 确保错误状态的正确性:验证失败显示红色
setVerifyResult(false);
setShowError(true);
setIsVerified(false);
// 增加尝试次数
setAttempts(prev => prev + 1);
// 1.5秒后重置滑块位置、隐藏错误提示并重置验证结果
setTimeout(() => {
setSliderPosition(0);
setShowError(false);
setVerifyResult(false);
setIsVerified(false);
}, 1500);
}
// 后端返回系统错误500
else if (code === 500) {
// 系统错误显示橙色
setVerifyResult('error');
setShowError(true);
setIsVerified(false);
// 增加尝试次数
setAttempts(prev => prev + 1);
// 1.5秒后重置滑块位置、隐藏错误提示并重置验证结果
setTimeout(() => {
setSliderPosition(0);
setShowError(false);
setVerifyResult(false);
setIsVerified(false);
}, 1500);
}
} catch (error) {
// 捕获验证请求异常
const errMsg = '验证请求失败: ' + (error as Error).message;
console.error(errMsg);
setMsg(errMsg);
setVerifyResult('error');
setShowError(true);
// 1.5秒后重置滑块位置并隐藏错误提示
setTimeout(() => {
setSliderPosition(0);
setShowError(false);
}, 1500);
} finally {
// 无论成功失败,都结束加载状态
setIsLoading(false);
}
}, [isDragging, isVerified, isLoading, sliderPosition, processId, onVerify, verifyResult]);
/**
* 鼠标按下事件处理
* 阻止默认行为,调用开始拖拽函数
* @param {React.MouseEvent} e - 鼠标事件对象
*/
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
handleStart(e.clientX);
};
/**
* 鼠标移动事件处理
* 阻止默认行为,调用拖拽移动函数
* @param {MouseEvent} e - 鼠标事件对象
*/
const handleMouseMove = useCallback((e: MouseEvent) => {
e.preventDefault();
handleMove(e.clientX);
}, [handleMove]);
/**
* 鼠标释放事件处理
* 阻止默认行为,调用结束拖拽函数
* @param {MouseEvent} e - 鼠标事件对象
*/
const handleMouseUp = useCallback((e: MouseEvent) => {
e.preventDefault();
handleEnd();
}, [handleEnd]);
/**
* 触摸开始事件处理
* 阻止默认行为,调用开始拖拽函数(适配移动端)
* @param {React.TouchEvent} e - 触摸事件对象
*/
const handleTouchStart = (e: React.TouchEvent) => {
e.preventDefault();
handleStart(e.touches[0].clientX);
};
/**
* 触摸移动事件处理
* 阻止默认行为,调用拖拽移动函数(适配移动端)
* @param {TouchEvent} e - 触摸事件对象
*/
const handleTouchMove = useCallback((e: TouchEvent) => {
e.preventDefault();
handleMove(e.touches[0].clientX);
}, [handleMove]);
/**
* 触摸结束事件处理
* 阻止默认行为,调用结束拖拽函数(适配移动端)
* @param {TouchEvent} e - 触摸事件对象
*/
const handleTouchEnd = useCallback((e: TouchEvent) => {
e.preventDefault();
handleEnd();
}, [handleEnd]);
/**
* 拖拽状态变化时绑定/解绑全局事件
* 当开始拖拽时为document绑定鼠标和触摸移动/结束事件;
* 当结束拖拽时,移除这些事件监听
*/
useEffect(() => {
if (isDragging) {
// 绑定鼠标事件
document.addEventListener('mousemove', handleMouseMove, { passive: false });
document.addEventListener('mouseup', handleMouseUp, { passive: false });
// 绑定触摸事件
document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('touchend', handleTouchEnd, { passive: false });
// 组件卸载或拖拽状态结束时,移除事件监听
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', handleTouchEnd);
};
}
}, [isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]);
/**
* 获取滑块显示的图标
* 根据不同状态(加载中、已验证、错误、默认)返回不同图标
* @returns {JSX.Element} 滑块图标
*/
const getSliderIcon = () => {
if (isLoading) {
// 加载中显示旋转动画
return <div className="w-5 h-5 border-2 border-blue-300 border-t-blue-600 rounded-full animate-spin" />;
}
// 验证成功时,无论其他状态如何,都显示对勾图标
if (isVerified) {
return <Check className="w-5 h-5 text-green-600" />;
}
// 验证失败或错误时显示叉号图标
if (showError || verifyResult === 'error') {
return <X className="w-5 h-5 text-red-600" />;
}
// 默认显示蓝色圆点
return <div className="w-3 h-3 bg-blue-500 rounded-full" />;
};
const getStatusText = () => {
if (isVerified) {
// 验证成功时优先显示成功消息
return msg;
}
if (verifyResult === 'error' || showError) {
// 错误或验证失败时显示后端返回的消息
return msg;
}
// 默认显示拖拽提示
return '拖动滑块完成拼图';
};
const getStatusColor = () => {
if (isVerified) return 'text-green-700';
if (verifyResult === 'error') return 'text-orange-700';
if (showError) return 'text-red-700';
return 'text-gray-600';
};
const getProgressColor = () => {
// 验证成功时,无论其他状态如何,都显示绿色渐变
if (isVerified) return 'bg-gradient-to-r from-green-400 to-green-500';
// 系统错误后端返回400/500显示橙色渐变
if (verifyResult === 'error') return 'bg-gradient-to-r from-orange-400 to-orange-500';
// 验证失败后端返回200但data=false显示红色渐变
if (showError && verifyResult !== 'error') return 'bg-gradient-to-r from-red-400 to-red-500';
// 默认显示蓝色渐变
return 'bg-gradient-to-r from-blue-400 to-blue-500';
};
return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md mx-auto transform transition-all duration-300 scale-100">
{/* 头部区域:显示标题和关闭按钮 */}
<div className="flex items-center justify-between p-6 border-b border-gray-100">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center">
<Shield className="w-4 h-4 text-white" />
</div>
<h3 className="text-lg font-semibold text-gray-900"></h3>
</div>
{/* 关闭按钮 */}
<button onClick={onClose} className="p-2 rounded-lg hover:bg-gray-100 transition-colors" title="关闭">
<X className="w-4 h-4 text-gray-500" />
</button>
</div>
{/* 显示验证码图片和滑块 */}
<div className="p-6">
<div className="relative">
{/* 背景图片容器尺寸300x200px与后端图片尺寸匹配 */}
<div className="relative bg-gray-200 rounded-lg w-[300px] h-[200px] mb-4 overflow-hidden mx-auto">
{backgroundImage && (
<img
src={backgroundImage}
alt="验证背景"
className="h-full w-full object-cover" // 图片填满容器
/>
)}
{/* 可移动拼图块 */}
{puzzleImage && (
<div
className={`absolute ${isDragging ? '' : 'transition-all duration-300'}`}
style={{
left: `${sliderPosition}px`, // 滑块x位置拼图左上角x坐标
top: `${puzzleY}px`, // 拼图y位置从后端获取拼图左上角y坐标
zIndex: 10,
}}
>
<img
src={puzzleImage}
alt="拼图块"
className={`${isVerified ? 'opacity-100' : 'opacity-90'}`}
style={{
filter: isVerified ? 'drop-shadow(0 0 10px rgba(34, 197, 94, 0.5))' : 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))'
}}
/>
</div>
)}
</div>
{/* 提示文本 */}
<p className="text-sm text-gray-600 mb-4 text-center">{getStatusText()}</p>
</div>
{/* 滑动轨道 */}
<div className="relative bg-gray-100 rounded-full h-12 overflow-hidden select-none" ref={trackRef} style={{ width: `${TRACK_WIDTH}px`, margin: '0 auto' }}>
{/* 进度条 */}
<div
className={`absolute left-0 top-0 h-full ${isDragging ? '' : 'transition-all duration-200 ease-out'} ${getProgressColor()}`}
style={{
width: `${sliderPosition + SLIDER_WIDTH}px`,
transform: isDragging ? 'scaleY(1.05)' : 'scaleY(1)',
transformOrigin: 'bottom'
}}
/>
{/* 滑块按钮 */}
<div
className={`absolute top-1 w-10 h-10 bg-white rounded-full shadow-lg cursor-pointer flex items-center justify-center ${isDragging ? '' : 'transition-all duration-200 ease-out'} select-none ${
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={verifyResult === 'error' ? undefined : handleMouseDown}
onTouchStart={verifyResult === 'error' ? undefined : handleTouchStart}
ref={sliderRef}
>
{getSliderIcon()}
</div>
{/* 轨道上的提示文字 */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<span className={`text-sm font-medium transition-all duration-300 ${
sliderPosition > 60 ? 'opacity-0 transform translate-x-4' : 'opacity-100 transform translate-x-0'
} ${getStatusColor()}`}>
{getStatusText()}
</span>
</div>
</div>
</div>
{/* 底部信息区域 */}
<div className="px-6 pb-6">
<div className="flex items-center justify-between text-xs text-gray-500">
<span>: {attempts}</span>
<span className="flex items-center space-x-1">
<Shield className="w-3 h-3" />
<span></span>
</span>
</div>
</div>
</div>
</div>
);
};
export default SliderCaptcha;

View File

@@ -42,19 +42,32 @@ export default function CharacterCard({
transition={{ duration: 0.2 }}
>
<div className="flex items-center justify-between mb-4">
{isEditing ? (
<input
type="text"
value={editName}
onChange={(e) => 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
/>
) : (
<h3 className="text-lg font-semibold text-gray-900 dark:text-white truncate flex-1">{profile.name}</h3>
)}
<div className="flex items-center gap-2 flex-1">
{isEditing ? (
<input
type="text"
value={editName}
onChange={(e) => 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
/>
) : (
<>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white truncate">{profile.name}</h3>
<motion.button
onClick={() => onEdit(profile.uuid, profile.name)}
className="text-gray-500 hover:text-orange-500 transition-colors"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
title="改名"
>
<PencilIcon className="w-4 h-4" />
</motion.button>
</>
)}
</div>
{profile.is_active && (
<span className="px-2 py-1 bg-gradient-to-r from-green-500 to-emerald-500 text-white text-xs rounded-full flex items-center space-x-1">
<CheckIcon className="w-3 h-3" />
@@ -68,9 +81,8 @@ export default function CharacterCard({
<SkinViewer
skinUrl={skinUrl}
isSlim={isSlim}
width={200}
height={200}
className="w-full h-full"
width={180}
height={180}
autoRotate={false}
/>
) : (
@@ -82,32 +94,10 @@ export default function CharacterCard({
<UserIcon className="w-10 h-10 text-white" />
</motion.div>
)}
{/* 皮肤选择按钮 */}
<motion.button
onClick={() => 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="选择皮肤"
>
<PencilIcon className="w-4 h-4" />
</motion.button>
</div>
{/* 操作按钮 */}
<div className="flex gap-2">
{!profile.is_active && (
<motion.button
onClick={() => 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 }}
>
使
</motion.button>
)}
{isEditing ? (
<>
<motion.button
@@ -130,13 +120,12 @@ export default function CharacterCard({
) : (
<>
<motion.button
onClick={() => 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 }}
>
<PencilIcon className="w-4 h-4 inline mr-1" />
</motion.button>
<motion.button
onClick={() => onDelete(profile.uuid)}

View File

@@ -68,7 +68,6 @@ export default function MySkinsTab({
key={skin.id}
texture={skin}
onViewDetails={handleViewDetails}
onToggleVisibility={onToggleVisibility}
customActions={
<div className="flex gap-2">
<button

View File

@@ -22,7 +22,7 @@ interface AuthContextType {
isLoading: boolean;
isAuthenticated: boolean;
login: (username: string, password: string) => Promise<void>;
register: (username: string, email: string, password: string, verificationCode: string) => Promise<void>;
register: (username: string, email: string, password: string, verificationCode: string, captchaId?: string) => Promise<void>;
logout: () => void;
updateUser: (userData: Partial<User>) => void;
refreshUser: () => Promise<void>;
@@ -106,7 +106,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
};
const register = async (username: string, email: string, password: string, verificationCode: string) => {
const register = async (username: string, email: string, password: string, verificationCode: string, captchaId?: string) => {
setIsLoading(true);
try {
const response = await fetch(`${API_BASE_URL}/auth/register`, {
@@ -119,6 +119,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
email,
password,
verification_code: verificationCode,
...(captchaId && { captcha_id: captchaId }),
}),
});

View File

@@ -1,4 +1,4 @@
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080/api/v1';
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || '/api/v1';
export interface Texture {
id: number;
@@ -313,3 +313,13 @@ export async function resetYggdrasilPassword(): Promise<ApiResponse<{
return response.json();
}
// 删除材质
export async function deleteTexture(id: number): Promise<ApiResponse<null>> {
const response = await fetch(`${API_BASE_URL}/texture/${id}`, {
method: 'DELETE',
headers: getAuthHeaders(),
});
return response.json();
}

View File

@@ -32,7 +32,7 @@ const config: Config = {
},
fontFamily: {
'minecraft': ['Minecraft', 'monospace'],
'sans': ['Inter', 'system-ui', 'sans-serif'],
'sans': ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
},
animation: {
'float': 'float 3s ease-in-out infinite',