From a9ff72a9bfd86eba55bffdb4720e41edd3dc56c4 Mon Sep 17 00:00:00 2001 From: lan Date: Thu, 4 Dec 2025 22:33:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=9A=AE=E8=82=A4=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E5=92=8C?= =?UTF-8?q?Yggdrasil=E5=AF=86=E7=A0=81=E9=87=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在用户资料页面添加皮肤选择和管理功能,支持上传、配置和移除皮肤 - 实现Yggdrasil密码重置功能,用户可生成新密码并显示 - 优化皮肤展示和选择界面,增强用户体验 - 更新SkinViewer组件,支持跑步和跳跃动画 --- package-lock.json | 101 +-------- src/app/profile/page.tsx | 351 +++++++++++++++++++++++++++-- src/app/skins/page.tsx | 345 ++++++++++++++-------------- src/components/SkinDetailModal.tsx | 315 ++++++++++++++++++++++++++ src/components/SkinViewer.tsx | 50 +++- src/lib/api.ts | 12 + 6 files changed, 879 insertions(+), 295 deletions(-) create mode 100644 src/components/SkinDetailModal.tsx diff --git a/package-lock.json b/package-lock.json index 744ec00..66cd025 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,71 +47,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@auth/core": { - "version": "0.34.3", - "resolved": "https://registry.npmmirror.com/@auth/core/-/core-0.34.3.tgz", - "integrity": "sha512-jMjY/S0doZnWYNV90x0jmU3B+UcrsfGYnukxYrRbj0CVvGI/MX3JbHsxSrx2d4mbnXaUsqJmAcDfoQWA6r0lOw==", - "license": "ISC", - "optional": true, - "dependencies": { - "@panva/hkdf": "^1.1.1", - "@types/cookie": "0.6.0", - "cookie": "0.6.0", - "jose": "^5.1.3", - "oauth4webapi": "^2.10.4", - "preact": "10.11.3", - "preact-render-to-string": "5.2.3" - }, - "peerDependencies": { - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.2", - "nodemailer": "^7" - }, - "peerDependenciesMeta": { - "@simplewebauthn/browser": { - "optional": true - }, - "@simplewebauthn/server": { - "optional": true - }, - "nodemailer": { - "optional": true - } - } - }, - "node_modules/@auth/core/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@auth/core/node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmmirror.com/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/@auth/core/node_modules/preact-render-to-string": { - "version": "5.2.3", - "resolved": "https://registry.npmmirror.com/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", - "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", - "license": "MIT", - "optional": true, - "dependencies": { - "pretty-format": "^3.8.0" - }, - "peerDependencies": { - "preact": ">=10" - } - }, "node_modules/@auth/prisma-adapter": { "version": "2.11.1", "resolved": "https://registry.npmmirror.com/@auth/prisma-adapter/-/prisma-adapter-2.11.1.tgz", @@ -211,7 +146,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -473,8 +407,7 @@ "version": "0.3.2", "resolved": "https://registry.npmmirror.com/@electric-sql/pglite/-/pglite-0.3.2.tgz", "integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.6", @@ -1568,7 +1501,6 @@ "resolved": "https://registry.npmmirror.com/@prisma/client/-/client-7.1.0.tgz", "integrity": "sha512-qf7GPYHmS/xybNiSOpzv9wBo+UwqfL2PeyX+08v+KVHDI0AlSCQIh5bBySkH3alu06NX9wy98JEnckhMHoMFfA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/client-runtime-utils": "7.1.0" }, @@ -2151,13 +2083,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmmirror.com/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT", - "optional": true - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", @@ -2194,7 +2119,6 @@ "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2282,7 +2206,6 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -2788,7 +2711,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3139,7 +3061,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3865,7 +3786,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4051,7 +3971,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4857,7 +4776,6 @@ "resolved": "https://registry.npmmirror.com/hono/-/hono-4.10.6.tgz", "integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -6212,16 +6130,6 @@ "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", "license": "MIT" }, - "node_modules/oauth4webapi": { - "version": "2.17.0", - "resolved": "https://registry.npmmirror.com/oauth4webapi/-/oauth4webapi-2.17.0.tgz", - "integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==", - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", @@ -6608,7 +6516,6 @@ "resolved": "https://registry.npmmirror.com/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -6648,7 +6555,6 @@ "integrity": "sha512-dy/3urE4JjhdiW5b09pGjVhGI7kPESK2VlCDrCqeYK5m5SslAtG5FCGnZWP7E8Sdg+Ow1wV2mhJH5RTFL5gEsw==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "7.1.0", "@prisma/dev": "0.15.0", @@ -6767,7 +6673,6 @@ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6777,7 +6682,6 @@ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7607,7 +7511,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7782,7 +7685,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8098,7 +8000,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 6ae314f..5db2902 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -35,9 +35,14 @@ import { getUserProfile, updateUserProfile, uploadTexture, + getTexture, + generateAvatarUploadUrl, + updateAvatarUrl, + resetYggdrasilPassword, type Texture, type Profile } from '@/lib/api'; +import SkinViewer from '@/components/SkinViewer'; interface UserProfile { id: number; @@ -78,6 +83,11 @@ export default function ProfilePage() { const [isUploadingAvatar, setIsUploadingAvatar] = useState(false); const [avatarUploadProgress, setAvatarUploadProgress] = useState(0); const [error, setError] = useState(null); + const [profileSkins, setProfileSkins] = useState>({}); + const [showSkinSelector, setShowSkinSelector] = useState(null); + const [yggdrasilPassword, setYggdrasilPassword] = useState(''); + const [showYggdrasilPassword, setShowYggdrasilPassword] = useState(false); + const [isResettingYggdrasilPassword, setIsResettingYggdrasilPassword] = useState(false); const { user, isAuthenticated, logout } = useAuth(); @@ -104,6 +114,34 @@ export default function ProfilePage() { const profilesResponse = await getProfiles(); if (profilesResponse.code === 200) { setProfiles(profilesResponse.data); + + // 加载每个角色的皮肤信息 + const skinPromises = profilesResponse.data + .filter(profile => profile.skin_id) + .map(async (profile) => { + try { + const skinResponse = await getTexture(profile.skin_id!); + if (skinResponse.code === 200 && skinResponse.data) { + return { + uuid: profile.uuid, + url: skinResponse.data.url, + isSlim: skinResponse.data.is_slim + }; + } + } catch (error) { + console.error(`加载角色 ${profile.uuid} 的皮肤失败:`, error); + } + return null; + }); + + const skinResults = await Promise.all(skinPromises); + const skinMap: Record = {}; + skinResults.forEach((result) => { + if (result) { + skinMap[result.uuid] = { url: result.url, isSlim: result.isSlim }; + } + }); + setProfileSkins(skinMap); } else { throw new Error(profilesResponse.message || '获取角色列表失败'); } @@ -163,6 +201,11 @@ export default function ProfilePage() { const response = await deleteProfile(uuid); if (response.code === 200) { setProfiles(prev => prev.filter(profile => profile.uuid !== uuid)); + setProfileSkins(prev => { + const newSkins = { ...prev }; + delete newSkins[uuid]; + return newSkins; + }); alert('角色删除成功!'); } else { throw new Error(response.message || '删除角色失败'); @@ -392,9 +435,9 @@ export default function ProfilePage() { if (!confirm('确定要删除头像吗?')) return; try { - const response = await updateAvatarUrl(''); + const response = await updateUserProfile({ avatar: '' }); if (response.code === 200) { - setUserProfile(prev => prev ? { ...prev, avatar: undefined } : null); + setUserProfile(response.data); alert('头像删除成功!'); } else { throw new Error(response.message || '删除头像失败'); @@ -405,6 +448,85 @@ 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); + alert('Yggdrasil密码重置成功!请妥善保管新密码。'); + } else { + throw new Error(response.message || '重置Yggdrasil密码失败'); + } + } catch (error) { + console.error('重置Yggdrasil密码失败:', error); + alert(error instanceof Error ? error.message : '重置Yggdrasil密码失败,请稍后重试'); + } finally { + setIsResettingYggdrasilPassword(false); + } + }; + + const handleAssignSkin = async (profileUuid: string, skinId: number) => { + try { + const response = await updateProfile(profileUuid, { skin_id: skinId }); + if (response.code === 200) { + // 更新角色数据 + setProfiles(prev => prev.map(profile => + profile.uuid === profileUuid ? response.data : profile + )); + + // 更新皮肤显示 + const skinResponse = await getTexture(skinId); + if (skinResponse.code === 200 && skinResponse.data) { + setProfileSkins(prev => ({ + ...prev, + [profileUuid]: { + url: skinResponse.data.url, + isSlim: skinResponse.data.is_slim + } + })); + } + + setShowSkinSelector(null); + alert('皮肤配置成功!'); + } else { + throw new Error(response.message || '配置皮肤失败'); + } + } catch (error) { + console.error('配置皮肤失败:', error); + alert(error instanceof Error ? error.message : '配置皮肤失败,请稍后重试'); + } + }; + + const handleRemoveSkin = async (profileUuid: string) => { + try { + const response = await updateProfile(profileUuid, { skin_id: undefined }); + if (response.code === 200) { + setProfiles(prev => prev.map(profile => + profile.uuid === profileUuid ? response.data : profile + )); + + // 移除皮肤显示 + setProfileSkins(prev => { + const newSkins = { ...prev }; + delete newSkins[profileUuid]; + return newSkins; + }); + + alert('皮肤移除成功!'); + } else { + throw new Error(response.message || '移除皮肤失败'); + } + } catch (error) { + console.error('移除皮肤失败:', error); + alert(error instanceof Error ? error.message : '移除皮肤失败,请稍后重试'); + } + }; + const sidebarVariants = { hidden: { x: -100, opacity: 0 }, visible: { x: 0, opacity: 1, transition: { duration: 0.5, ease: "easeOut" as const } } @@ -680,6 +802,118 @@ export default function ProfilePage() { )} + {/* Skin Selector Modal */} + + {showSkinSelector && ( + + +
+

选择皮肤

+ +
+ + {mySkins.length === 0 ? ( +
+ +

暂无皮肤

+

您还没有上传任何皮肤

+ +
+ ) : ( + <> +
+ +
+ +
+ {mySkins.map((skin) => ( + handleAssignSkin(showSkinSelector, skin.id)} + > +
+ {skin.type === 'SKIN' ? ( + + ) : ( +
+
+
+ 🧥 +
+

披风

+
+
+ )} + {!skin.is_public && ( +
+ 私密 +
+ )} +
+ +
+

{skin.name}

+

+ {skin.type === 'SKIN' ? (skin.is_slim ? '细臂模型' : '经典模型') : '披风'} +

+
+ + + {skin.download_count} + + + + {skin.favorite_count} + +
+
+
+ ))} +
+ + )} +
+
+ )} +
+ {profiles.length === 0 ? (
@@ -716,8 +950,21 @@ export default function ProfilePage() { )}
-
-
+
+ {profileSkins[profile.uuid] ? ( + + ) : ( +
+
+
+ )}
@@ -729,6 +976,12 @@ export default function ProfilePage() { 使用 )} +