Profile 页面
This commit is contained in:
parent
5bbe89a3a6
commit
867ca4c9de
@ -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": {
|
||||
|
59
frontend/pnpm-lock.yaml
generated
59
frontend/pnpm-lock.yaml
generated
@ -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:
|
||||
|
@ -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 />
|
||||
|
@ -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
|
||||
}
|
@ -32,3 +32,11 @@ export interface ApiServerInfo {
|
||||
serverName: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface YggProfile {
|
||||
name: string
|
||||
properties: {
|
||||
name: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
17
frontend/src/utils/skin.ts
Normal file
17
frontend/src/utils/skin.ts
Normal 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]
|
||||
}
|
@ -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))
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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>
|
||||
// </>}
|
||||
// </>
|
||||
// )
|
||||
// }
|
@ -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
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user