From 867ca4c9de3ec69c0818b9116ad64e386679bfe8 Mon Sep 17 00:00:00 2001 From: xmdhs Date: Mon, 2 Oct 2023 16:59:47 +0800 Subject: [PATCH] =?UTF-8?q?Profile=20=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 2 + frontend/pnpm-lock.yaml | 59 +++++++++++ frontend/src/Route.tsx | 6 +- frontend/src/apis/apis.ts | 12 ++- frontend/src/apis/model.ts | 8 ++ frontend/src/utils/skin.ts | 17 +++ frontend/src/views/Layout.tsx | 13 ++- frontend/src/views/User.tsx | 38 ------- frontend/src/views/profile/Profile.tsx | 138 +++++++++++++++++++++++++ handle/yggdrasil/texture.go | 10 +- 10 files changed, 256 insertions(+), 47 deletions(-) create mode 100644 frontend/src/utils/skin.ts delete mode 100644 frontend/src/views/User.tsx diff --git a/frontend/package.json b/frontend/package.json index 607d6f0..a8d48d2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.16.0", + "react-skinview3d": "^5.0.2", + "skinview3d": "^3.0.1", "tilg": "^0.1.1" }, "devDependencies": { diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index cec0839..1f51ee0 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -38,6 +38,12 @@ dependencies: react-router-dom: specifier: ^6.16.0 version: 6.16.0(react-dom@18.2.0)(react@18.2.0) + react-skinview3d: + specifier: ^5.0.2 + version: 5.0.2(react-dom@18.2.0)(react@18.2.0) + skinview3d: + specifier: ^3.0.1 + version: 3.0.1 tilg: specifier: ^0.1.1 version: 0.1.1(react@18.2.0) @@ -921,6 +927,23 @@ packages: resolution: {integrity: sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==} dev: true + /@types/stats.js@0.17.1: + resolution: {integrity: sha512-OgfYE1x2w1jRUXzzKABX+kOdwz2y9PE0uSwnZabkWfJTWOzm7Pvfm4JI2xqRE0q2nwUe2jZLWcrcnhd9lQU63w==} + dev: false + + /@types/three@0.156.0: + resolution: {integrity: sha512-733bXDSRdlrxqOmQuOmfC1UBRuJ2pREPk8sWnx9MtIJEVDQMx8U0NQO5MVVaOrjzDPyLI+cFPim2X/ss9v0+LQ==} + dependencies: + '@types/stats.js': 0.17.1 + '@types/webxr': 0.5.5 + fflate: 0.6.10 + meshoptimizer: 0.18.1 + dev: false + + /@types/webxr@0.5.5: + resolution: {integrity: sha512-HVOsSRTQYx3zpVl0c0FBmmmcY/60BkQLzVnpE9M1aG4f2Z0aKlBWfj4XZ2zr++XNBfkQWYcwhGlmuu44RJPDqg==} + dev: false + /@typescript-eslint/eslint-plugin@6.7.0(@typescript-eslint/parser@6.7.0)(eslint@8.49.0)(typescript@5.2.2): resolution: {integrity: sha512-gUqtknHm0TDs1LhY12K2NA3Rmlmp88jK9Tx8vGZMfHeNMLE3GH2e9TRub+y+SOjuYgtOmok+wt1AyDPZqxbNag==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1475,6 +1498,10 @@ packages: reusify: 1.0.4 dev: true + /fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + dev: false + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1772,6 +1799,10 @@ packages: engines: {node: '>= 8'} dev: true + /meshoptimizer@0.18.1: + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} + dev: false + /micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} @@ -1957,6 +1988,18 @@ packages: react: 18.2.0 dev: false + /react-skinview3d@5.0.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-4vB1pPm9u0cX03hWZNCcceQH+yMuZee7xHqqq2Kc0ur6l39+haDEvrJAbLZv6bKvJVj5Gu3Mur1P8HPoy0/HhA==} + engines: {node: '>=16'} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + skinview3d: 3.0.1 + dev: false + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -2056,6 +2099,18 @@ packages: engines: {node: '>=8'} dev: true + /skinview-utils@0.7.1: + resolution: {integrity: sha512-4eLrMqR526ehlZbsd8SuZ/CHpS9GiH0xUMoV+PYlJVi95ZFz5HJu7Spt5XYa72DRS7wgt5qquvHZf0XZJgmu9Q==} + dev: false + + /skinview3d@3.0.1: + resolution: {integrity: sha512-2LUSkzGxlZrTQelGT10jcW4TLiFTg5aZqXMEuqAFoWtk3qtaNu0qRFtwK5dN8zEXyKUJ3xlxah5eGtKY/NifQg==} + dependencies: + '@types/three': 0.156.0 + skinview-utils: 0.7.1 + three: 0.156.1 + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2110,6 +2165,10 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /three@0.156.1: + resolution: {integrity: sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ==} + dev: false + /tilg@0.1.1(react@18.2.0): resolution: {integrity: sha512-0uHyTAUM0tJL792LeviRPFkJtCbF6Za3/hbbnRmWGUaicOhbJ0IpvBViXiXTF7nk6R0L6vve2XLesQzn5jEVng==} peerDependencies: diff --git a/frontend/src/Route.tsx b/frontend/src/Route.tsx index 1ce1a22..81f03f7 100644 --- a/frontend/src/Route.tsx +++ b/frontend/src/Route.tsx @@ -2,7 +2,7 @@ import { Routes, Route, createBrowserRouter, RouterProvider } from "react-router import { ScrollRestoration } from "react-router-dom"; import Login from '@/views/Login' import Register from '@/views/Register' -import User from '@/views/User' +import Profile from '@/views/profile/Profile' import Layout from '@/views/Layout' const router = createBrowserRouter([ @@ -16,8 +16,8 @@ function Root() { }> } /> } /> - }> - + } /> + diff --git a/frontend/src/apis/apis.ts b/frontend/src/apis/apis.ts index d5c5a3f..4c50a00 100644 --- a/frontend/src/apis/apis.ts +++ b/frontend/src/apis/apis.ts @@ -1,4 +1,4 @@ -import type { tokenData, ApiUser, ApiServerInfo } from '@/apis/model' +import type { tokenData, ApiUser, ApiServerInfo, YggProfile } from '@/apis/model' import { apiGet } from '@/apis/utils' export async function login(username: string, password: string) { @@ -43,4 +43,14 @@ export async function userInfo(token: string) { export async function serverInfo() { const v = await fetch(import.meta.env.VITE_APIADDR + "/api/yggdrasil") return await v.json() as ApiServerInfo +} + +export async function yggProfile(uuid: string) { + if (uuid == "") return + const v = await fetch(import.meta.env.VITE_APIADDR + "/api/yggdrasil/sessionserver/session/minecraft/profile/" + uuid) + const data = await v.json() + if (!v.ok) { + throw new Error(data?.errorMessage) + } + return data as YggProfile } \ No newline at end of file diff --git a/frontend/src/apis/model.ts b/frontend/src/apis/model.ts index 7c2a650..03b0342 100644 --- a/frontend/src/apis/model.ts +++ b/frontend/src/apis/model.ts @@ -32,3 +32,11 @@ export interface ApiServerInfo { serverName: string } } + +export interface YggProfile { + name: string + properties: { + name: string + value: string + }[] +} \ No newline at end of file diff --git a/frontend/src/utils/skin.ts b/frontend/src/utils/skin.ts new file mode 100644 index 0000000..53b9295 --- /dev/null +++ b/frontend/src/utils/skin.ts @@ -0,0 +1,17 @@ +import { YggProfile } from "@/apis/model"; + +export function decodeSkin(y: YggProfile) { + if (y.properties.length == 0) { + return ["", "", ""] + } + const p = y.properties.find(v => v.name == "textures") + if (!p?.value || p?.value == "") { + return ["", "", ""] + } + const textures = JSON.parse(atob(p.value)) + + const skin = textures?.textures?.SKIN?.url as string ?? "" + const cape = textures?.textures?.CAPE?.url as string ?? "" + const model = textures?.textures?.SKIN?.metadata?.model as string ?? "default" + return [skin, cape, model] +} \ No newline at end of file diff --git a/frontend/src/views/Layout.tsx b/frontend/src/views/Layout.tsx index e2cf0f5..f3a73df 100644 --- a/frontend/src/views/Layout.tsx +++ b/frontend/src/views/Layout.tsx @@ -21,7 +21,7 @@ import { AccountCircle } from '@mui/icons-material'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import { token, user } from '@/store/store'; -import { useAtom, useAtomValue } from 'jotai'; +import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; import Button from '@mui/material/Button'; import { useNavigate } from "react-router-dom"; import { useRequest, useMemoizedFn } from 'ahooks'; @@ -33,7 +33,9 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import useTilg from 'tilg' import { ApiErr } from '@/apis/error'; import Typography from '@mui/material/Typography'; +import Container from '@mui/material/Container'; +export const AlertErr = atom("") const drawerWidth = 240; @@ -52,7 +54,7 @@ export default function Layout() { const isLg = useMediaQuery(theme.breakpoints.up('lg')) const [open, setOpen] = React.useState(false); const nowToken = useAtomValue(token) - const [err, setErr] = React.useState(""); + const [err, setErr] = useAtom(AlertErr) const navigate = useNavigate(); @@ -140,7 +142,10 @@ export default function Layout() { flexGrow: 1, bgcolor: 'background.default', p: 3 }} > - + + + + ) @@ -151,12 +156,14 @@ const MyToolbar = memo((p: { setOpen: (v: boolean) => void }) => { const [anchorEl, setAnchorEl] = React.useState(null); const navigate = useNavigate(); const [, setToken] = useAtom(token) + const setErr = useSetAtom(AlertErr) const server = useRequest(serverInfo, { cacheKey: "/api/yggdrasil", cacheTime: 100000, onError: e => { console.warn(e) + setErr(String(e)) } }) diff --git a/frontend/src/views/User.tsx b/frontend/src/views/User.tsx deleted file mode 100644 index 5e8ec95..0000000 --- a/frontend/src/views/User.tsx +++ /dev/null @@ -1,38 +0,0 @@ - - - -export default function User(){ - return (<> - ) - -} - -// import { token, username } from "@/store/store" -// import { useRequest } from "ahooks" -// import { useAtomValue } from "jotai" -// import { userInfo } from '@/apis/apis' - - -// export default function User() { -// const nowToken = useAtomValue(token) -// const nowUsername = useAtomValue(username) - -// const { data, error } = useRequest(() => userInfo(nowToken), { -// refreshDeps: [nowToken], -// cacheKey: "/api/v1/user/reg", -// cacheTime: 10000 -// }) - - -// return ( -// <> -//

你好: {nowUsername}

-//

token: {nowToken}

-// {error && String(error)} -// {!error && <> -//

uid: {data?.data.uid}

-//

uuid: {data?.data.uuid}

-// } -// -// ) -// } \ No newline at end of file diff --git a/frontend/src/views/profile/Profile.tsx b/frontend/src/views/profile/Profile.tsx index e69de29..431aaac 100644 --- a/frontend/src/views/profile/Profile.tsx +++ b/frontend/src/views/profile/Profile.tsx @@ -0,0 +1,138 @@ +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import CardHeader from '@mui/material/CardHeader'; +import { useHover, useRequest } from 'ahooks'; +import { ApiErr } from '@/apis/error'; +import { token } from '@/store/store'; +import { useAtomValue, useSetAtom } from 'jotai'; +import { userInfo, yggProfile } from '@/apis/apis'; +import { AlertErr } from '@/views/Layout'; +import { useNavigate } from 'react-router-dom'; +import Box from '@mui/material/Box'; +import { memo, useEffect, useRef, useState } from 'react'; +import { decodeSkin } from '@/utils/skin'; +import ReactSkinview3d from "react-skinview3d" +import type { ReactSkinview3dOptions } from "react-skinview3d" +import { WalkingAnimation } from "skinview3d" +import type { SkinViewer } from "skinview3d" +import Skeleton from '@mui/material/Skeleton'; +import useTilg from 'tilg'; + +const Profile = () => { + const nowToken = useAtomValue(token) + const navigate = useNavigate(); + const setErr = useSetAtom(AlertErr) + const [textures, setTextures] = useState({ skin: "", cape: "", model: "default" }) + + const userinfo = useRequest(() => userInfo(nowToken), { + refreshDeps: [nowToken], + cacheKey: "/api/v1/user", + cacheTime: 10000, + onError: e => { + if (e instanceof ApiErr && e.code == 5) { + navigate("/login") + } + console.warn(e) + setErr(String(e)) + } + }) + + const SkinInfo = useRequest(() => yggProfile(userinfo.data?.uuid ?? ""), { + onError: e => { + console.warn(e) + setErr(String(e)) + }, + refreshDeps: [userinfo.data?.uuid], + }) + + useEffect(() => { + if (!SkinInfo.data) return + const [skin, cape, model] = decodeSkin(SkinInfo.data) + setTextures({ cape: cape, skin: skin, model: model }) + }, [SkinInfo.data]) + + + + return ( + <> + + + + + 用户名 + {SkinInfo.loading || userinfo.loading ? : SkinInfo.data?.name} + uid + {userinfo.loading ? : userinfo.data?.uid} + uuid + {userinfo.loading ? : userinfo.data?.uuid} + + {/* + + */} + + + + + { + SkinInfo.loading ? + : (textures.skin != "" || textures.cape != "") && ( + ) + } + + + + + + + + + 本站 Yggdrasil API 地址 + {import.meta.env.VITE_APIADDR + "/api/yggdrasil"} + + + + + + ) +} + + + +const MySkin = memo((p: ReactSkinview3dOptions) => { + const refSkinview3d = useRef(null); + const skinisHovering = useHover(refSkinview3d); + const skinview3dView = useRef(null); + + useEffect(() => { + if (skinview3dView.current) { + skinview3dView.current.autoRotate = !skinisHovering + } + if (skinview3dView.current?.animation) { + skinview3dView.current.animation.paused = skinisHovering + } + }, [skinisHovering]) + + useTilg(`refSkinview3d= `, refSkinview3d, `skinisHovering=${skinisHovering}`); + return
+ [v.viewer.animation = new WalkingAnimation(), v.viewer.autoRotate = true, skinview3dView.current = v.viewer]} + /> +
+}) + +export default Profile \ No newline at end of file diff --git a/handle/yggdrasil/texture.go b/handle/yggdrasil/texture.go index bffaad6..6475065 100644 --- a/handle/yggdrasil/texture.go +++ b/handle/yggdrasil/texture.go @@ -118,11 +118,17 @@ func (y *Yggdrasil) PutTexture() httprouter.Handle { func getUUIDbyParams(ctx context.Context, p httprouter.Params, l *slog.Logger, w http.ResponseWriter) (string, string, bool) { uuid := p.ByName("uuid") textureType := p.ByName("textureType") - if uuid == "" || textureType == "" { - l.DebugContext(ctx, "路径中缺少参数 uuid / textureType") + if uuid == "" { + l.DebugContext(ctx, "路径中缺少参数 uuid") handleYgError(ctx, w, yggdrasil.Error{ErrorMessage: "路径中缺少参数 uuid / textureType"}, 400) return "", "", false } + if textureType != "skin" && textureType != "cape" { + l.DebugContext(ctx, "上传类型错误") + handleYgError(ctx, w, yggdrasil.Error{ErrorMessage: "上传类型错误"}, 400) + return "", "", false + + } return uuid, textureType, true }