Profile 页面

This commit is contained in:
xmdhs 2023-10-02 16:59:47 +08:00
parent 5bbe89a3a6
commit 867ca4c9de
No known key found for this signature in database
GPG Key ID: E809D6D43DEFCC95
10 changed files with 256 additions and 47 deletions

View File

@ -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": {

View File

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

View File

@ -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() {
<Route path="/" element={<Layout />}>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/profile" element={<User />}>
</Route>
<Route path="/profile" element={<Profile />} />
</Route>
</Routes>
<ScrollRestoration />

View File

@ -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) {
@ -44,3 +44,13 @@ 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
}

View File

@ -32,3 +32,11 @@ export interface ApiServerInfo {
serverName: string
}
}
export interface YggProfile {
name: string
properties: {
name: string
value: string
}[]
}

View File

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

View File

@ -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
}}
>
<Outlet />
<Toolbar />
<Container maxWidth="lg">
<Outlet />
</Container>
</Box>
</Box>
</>)
@ -151,12 +156,14 @@ const MyToolbar = memo((p: { setOpen: (v: boolean) => void }) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(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))
}
})

View File

@ -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 (
// <>
// <p>你好: {nowUsername}</p>
// <p>token: {nowToken} </p>
// {error && String(error)}
// {!error && <>
// <p>uid: {data?.data.uid}</p>
// <p>uuid: {data?.data.uuid}</p>
// </>}
// </>
// )
// }

View File

@ -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 (
<>
<Box sx={{
display: "grid", gap: "1em", gridTemplateAreas: {
lg: '"a b d" "c b d"',
xs: '"a" "b" "c" "d"'
}, gridTemplateColumns: { lg: "1fr 1fr auto" }
}}>
<Card sx={{ gridArea: "a" }}>
<CardHeader title="信息" />
<CardContent sx={{ display: "grid", gridTemplateColumns: "4em auto" }}>
<Typography></Typography>
<Typography>{SkinInfo.loading || userinfo.loading ? <Skeleton /> : SkinInfo.data?.name}</Typography>
<Typography>uid</Typography>
<Typography sx={{ wordBreak: 'break-all' }}>{userinfo.loading ? <Skeleton /> : userinfo.data?.uid}</Typography>
<Typography>uuid</Typography>
<Typography sx={{ wordBreak: 'break-all' }}>{userinfo.loading ? <Skeleton /> : userinfo.data?.uuid}</Typography>
</CardContent>
{/* <CardActions>
<Button size="small"></Button>
</CardActions> */}
</Card>
<Card sx={{ gridArea: "b" }}>
<CardHeader title="皮肤" />
<CardContent sx={{ display: "flex", justifyContent: 'center' }}>
{
SkinInfo.loading ? <Skeleton variant="rectangular" width={250} height={250} />
: (textures.skin != "" || textures.cape != "") && (
<MySkin
skinUrl={textures.skin}
capeUrl={textures.cape}
height="250"
width="250"
options={{ model: textures.model as "default" | "slim" }}
/>)
}
</CardContent>
<CardActions>
<Button size="small"></Button>
</CardActions>
</Card>
<Card sx={{ gridArea: "c" }}>
<CardHeader title="启动器设置" />
<CardContent>
<Typography> Yggdrasil API </Typography>
<code>{import.meta.env.VITE_APIADDR + "/api/yggdrasil"}</code>
</CardContent>
</Card>
<Box sx={{ gridArea: "d" }}></Box>
</Box >
</>
)
}
const MySkin = memo((p: ReactSkinview3dOptions) => {
const refSkinview3d = useRef(null);
const skinisHovering = useHover(refSkinview3d);
const skinview3dView = useRef<SkinViewer | null>(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 <div ref={refSkinview3d}>
<ReactSkinview3d
{...p}
onReady={v => [v.viewer.animation = new WalkingAnimation(), v.viewer.autoRotate = true, skinview3dView.current = v.viewer]}
/>
</div>
})
export default Profile

View File

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