上传皮肤

This commit is contained in:
xmdhs 2023-10-03 21:05:59 +08:00
parent 6c2080f53a
commit 2363283cc5
No known key found for this signature in database
GPG Key ID: E809D6D43DEFCC95
11 changed files with 280 additions and 90 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>皮肤站</title>
</head>
<body>
<div id="root"></div>

View File

@ -18,6 +18,7 @@
"ahooks": "^3.7.8",
"immer": "^10.0.2",
"jotai": "^2.4.2",
"mui-file-input": "^3.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.16.0",

View File

@ -29,6 +29,9 @@ dependencies:
jotai:
specifier: ^2.4.2
version: 2.4.2(@types/react@18.2.21)(react@18.2.0)
mui-file-input:
specifier: ^3.0.2
version: 3.0.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/icons-material@5.14.9)(@mui/material@5.14.10)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
@ -1821,6 +1824,30 @@ packages:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/mui-file-input@3.0.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/icons-material@5.14.9)(@mui/material@5.14.10)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-58Jp3+f5MUXjhZjlLfYFOEOBICnLoFC4x0G1+y411JsGBjZQ1lgICv7KQVgP5aF+IRvhJ1vfI6KpbnmqwRKXoA==}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@mui/icons-material': ^5.0.0
'@mui/material': ^5.0.0
'@types/react': ^18.0.0
react: ^18.0.0
react-dom: ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@emotion/react': 11.11.1(@types/react@18.2.21)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.21)(react@18.2.0)
'@mui/icons-material': 5.14.9(@mui/material@5.14.10)(@types/react@18.2.21)(react@18.2.0)
'@mui/material': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.21
pretty-bytes: 6.1.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/nanoid@3.3.6:
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -1930,6 +1957,11 @@ packages:
engines: {node: '>= 0.8.0'}
dev: true
/pretty-bytes@6.1.1:
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
engines: {node: ^14.13.1 || >=16.0.0}
dev: false
/prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
dependencies:

View File

@ -0,0 +1,16 @@
import { serverInfo } from '@/apis/apis'
import { useTitle as auseTitle, useRequest } from 'ahooks'
import { useEffect } from 'react'
export default function useTitle(title: string) {
const { data, error } = useRequest(serverInfo, {
cacheKey: "/api/yggdrasil",
staleTime: 60000,
})
useEffect(() => {
error && console.warn(error)
}, [error])
auseTitle(title + " - " + data?.meta.serverName ?? "", {
restoreOnUnmount: true
})
}

View File

@ -19,7 +19,7 @@ import { AccountCircle } from '@mui/icons-material';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { LayoutAlertErr, token, user } from '@/store/store';
import { useAtom, useAtomValue, useSetAtom } 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';
@ -37,6 +37,7 @@ import SettingsIcon from '@mui/icons-material/Settings';
import useTilg from 'tilg'
const drawerWidth = 240;
const DrawerOpen = atom(false)
const DrawerHeader = styled('div')(({ theme }) => ({
display: 'flex',
@ -53,51 +54,9 @@ interface ListItem {
link: string
}
const Layout = memo(function Layout() {
const theme = useTheme();
const isLg = useMediaQuery(theme.breakpoints.up('lg'))
const [open, setOpen] = React.useState(false);
const nowToken = useAtomValue(token)
const [err, setErr] = useAtom(LayoutAlertErr)
const navigate = useNavigate();
const userinfo = useRequest(() => userInfo(nowToken), {
refreshDeps: [nowToken],
cacheKey: "/api/v1/user",
onError: e => {
if (e instanceof ApiErr && e.code == 5) {
navigate("/login")
}
console.warn(e)
setErr(String(e))
}
})
const userDrawerList = React.useMemo(() => [
{
icon: <PersonIcon />,
title: '个人信息',
link: '/profile'
},
{
icon: <SettingsIcon />,
title: '皮肤设置',
link: '/textures'
},
{
icon: <SecurityIcon />,
title: '安全设置',
link: '/setting'
}
] as ListItem[], [])
const adminDrawerList = React.useMemo(() => [
{
icon: <PersonIcon />,
title: 'test'
}
] as ListItem[], [])
useTilg()
@ -108,37 +67,9 @@ const Layout = memo(function Layout() {
zIndex: { lg: theme.zIndex.drawer + 1 }
}}
>
<MyToolbar setOpen={setOpen}></MyToolbar>
<MyToolbar />
</AppBar>
{userinfo.data && (
<Drawer
sx={{
width: drawerWidth,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: drawerWidth,
boxSizing: 'border-box',
},
}}
variant={isLg ? "persistent" : "temporary"}
anchor="left"
open={open || isLg}
onClose={() => setOpen(false)}
>
<DrawerHeader>
<IconButton onClick={() => setOpen(false)}>
{theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
</IconButton>
</DrawerHeader>
<Divider />
<MyList list={userDrawerList} />
{userinfo.data?.is_admin && (
<>
<Divider />
<MyList list={adminDrawerList} />
</>)}
</Drawer>
)}
<MyDrawer />
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={err != ""} onClose={() => setErr("")} >
<Alert onClose={() => setErr("")} severity="error">{err}</Alert>
</Snackbar>
@ -157,15 +88,17 @@ const Layout = memo(function Layout() {
</>)
})
const MyToolbar = memo(function MyToolbar(p: { setOpen: (v: boolean) => void }) {
const MyToolbar = memo(function MyToolbar() {
const [nowUser, setNowUser] = useAtom(user)
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const navigate = useNavigate();
const [, setToken] = useAtom(token)
const setErr = useSetAtom(LayoutAlertErr)
const setOpen = useSetAtom(DrawerOpen)
const server = useRequest(serverInfo, {
cacheKey: "/api/yggdrasil",
staleTime: 60000,
onError: e => {
console.warn(e)
setErr(String(e))
@ -180,6 +113,8 @@ const MyToolbar = memo(function MyToolbar(p: { setOpen: (v: boolean) => void })
navigate("/")
})
useTilg()
return (
<>
<Toolbar>
@ -190,7 +125,7 @@ const MyToolbar = memo(function MyToolbar(p: { setOpen: (v: boolean) => void })
color="inherit"
aria-label="menu"
sx={{ mr: 2, display: { lg: 'none' } }}
onClick={() => p.setOpen(true)}
onClick={() => setOpen(true)}
>
<MenuIcon />
</IconButton >
@ -270,4 +205,85 @@ const MyListItem = function MyListItem(p: ListItem) {
)
}
const MyDrawer = function MyDrawer() {
const nowToken = useAtomValue(token)
const setErr = useSetAtom(LayoutAlertErr)
const navigate = useNavigate();
const theme = useTheme();
const isLg = useMediaQuery(theme.breakpoints.up('lg'))
const [open, setOpen] = useAtom(DrawerOpen)
const userinfo = useRequest(() => userInfo(nowToken), {
refreshDeps: [nowToken],
cacheKey: "/api/v1/user" + nowToken,
staleTime: 60000,
onError: e => {
if (e instanceof ApiErr && e.code == 5) {
navigate("/login")
}
console.warn(e)
setErr(String(e))
},
})
const userDrawerList = React.useMemo(() => [
{
icon: <PersonIcon />,
title: '个人信息',
link: '/profile'
},
{
icon: <SettingsIcon />,
title: '皮肤设置',
link: '/textures'
},
{
icon: <SecurityIcon />,
title: '安全设置',
link: '/setting'
}
] as ListItem[], [])
const adminDrawerList = React.useMemo(() => [
{
icon: <PersonIcon />,
title: 'test'
}
] as ListItem[], [])
useTilg()
return (<>
{userinfo.data && (
<Drawer
sx={{
width: drawerWidth,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: drawerWidth,
boxSizing: 'border-box',
},
}}
variant={isLg ? "persistent" : "temporary"}
anchor="left"
open={open || isLg}
onClose={() => setOpen(false)}
>
<DrawerHeader>
<IconButton onClick={() => setOpen(false)}>
{theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
</IconButton>
</DrawerHeader>
<Divider />
<MyList list={userDrawerList} />
{userinfo.data?.is_admin && (
<>
<Divider />
<MyList list={adminDrawerList} />
</>)}
</Drawer>
)}
</>)
}
export default Layout

View File

@ -17,6 +17,7 @@ import { login } from '@/apis/apis'
import { Link as RouterLink, useNavigate } from "react-router-dom";
import Loading from '@/components/Loading'
import CheckInput, { refType } from '@/components/CheckInput'
import useTitle from '@/hooks/useTitle';
@ -27,6 +28,7 @@ export default function SignIn() {
const setUserInfo = useSetAtom(user)
const checkList = React.useRef<Map<string, refType>>(new Map<string, refType>())
const navigate = useNavigate();
useTitle("登录")
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
@ -50,7 +52,7 @@ export default function SignIn() {
uuid: v.selectedProfile.uuid,
name: v.selectedProfile.name,
})
navigate("/")
navigate("/profile")
}).
catch(v => [setErr(String(v)), console.warn(v)]).
finally(() => setLoading(false))

View File

@ -18,6 +18,7 @@ import Loading from '@/components/Loading'
import { useNavigate } from "react-router-dom";
import CaptchaWidget from '@/components/CaptchaWidget';
import type { refType as CaptchaWidgetRef } from '@/components/CaptchaWidget'
import useTitle from '@/hooks/useTitle';
export default function SignUp() {
const [regErr, setRegErr] = useState("");
@ -25,6 +26,7 @@ export default function SignUp() {
const [captchaToken, setCaptchaToken] = useState("");
const captchaRef = useRef<CaptchaWidgetRef>(null)
const [loading, setLoading] = useState(false);
useTitle("注册")
const checkList = React.useRef<Map<string, refType>>(new Map<string, refType>())

View File

@ -4,30 +4,34 @@ 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 { useHover, useRequest, useUnmount } from 'ahooks';
import { ApiErr } from '@/apis/error';
import { LayoutAlertErr, token } from '@/store/store';
import { useAtomValue, useSetAtom } from 'jotai';
import { userInfo, yggProfile } from '@/apis/apis';
import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
import { memo, useEffect, useRef, useState } from 'react';
import { 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';
import useTitle from '@/hooks/useTitle';
const Profile = memo(function Profile() {
const Profile = function Profile() {
const nowToken = useAtomValue(token)
const navigate = useNavigate();
const setErr = useSetAtom(LayoutAlertErr)
const [textures, setTextures] = useState({ skin: "", cape: "", model: "default" })
useTitle("个人信息")
const userinfo = useRequest(() => userInfo(nowToken), {
refreshDeps: [nowToken],
cacheKey: "/api/v1/user",
cacheKey: "/api/v1/user" + nowToken,
staleTime: 60000,
onError: e => {
if (e instanceof ApiErr && e.code == 5) {
navigate("/login")
@ -53,6 +57,7 @@ const Profile = memo(function Profile() {
}, [SkinInfo.data])
useTilg()
return (
<>
@ -106,10 +111,10 @@ const Profile = memo(function Profile() {
</Box >
</>
)
})
}
const MySkin = memo(function MySkin(p: ReactSkinview3dOptions) {
const MySkin = function MySkin(p: ReactSkinview3dOptions) {
const refSkinview3d = useRef(null);
const skinisHovering = useHover(refSkinview3d);
const skinview3dView = useRef<SkinViewer | null>(null);
@ -123,13 +128,17 @@ const MySkin = memo(function MySkin(p: ReactSkinview3dOptions) {
}
}, [skinisHovering])
useUnmount(() => {
skinview3dView.current?.dispose()
})
return <div ref={refSkinview3d}>
<ReactSkinview3d
{...p}
onReady={v => [v.viewer.animation = new WalkingAnimation(), v.viewer.autoRotate = true, skinview3dView.current = v.viewer]}
/>
</div>
})
}
function getYggRoot() {
const u = new URL((import.meta.env.VITE_APIADDR ?? location.origin) + "/api/yggdrasil")

View File

@ -1,9 +1,121 @@
import { memo } from "react";
import { useEffect, useRef, useState } from "react";
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import FormControlLabel from "@mui/material/FormControlLabel";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import Button from "@mui/material/Button";
import { CardHeader } from "@mui/material";
import useTilg from "tilg";
import useTitle from "@/hooks/useTitle";
import { MuiFileInput } from 'mui-file-input'
import Box from "@mui/material/Box";
import ReactSkinview3d from "react-skinview3d";
import { useUnmount } from "ahooks";
import { SkinViewer } from "skinview3d";
const Textures = memo(function Textures() {
return (<>
<p></p>
</>)
const Textures = function Textures() {
const [redioValue, setRedioValue] = useState("skin")
useTitle("上传皮肤")
const [file, setFile] = useState<File | null>(null)
const skin = useRef({
skinUrl: "",
capeUrl: "",
})
const skinview3dView = useRef<SkinViewer | null>(null);
useUnmount(() => {
skin.current.skinUrl && URL.revokeObjectURL(skin.current.skinUrl)
skin.current.capeUrl && URL.revokeObjectURL(skin.current.capeUrl)
})
useEffect(() => {
if (file) {
const nu = URL.createObjectURL(file)
skin.current.skinUrl && URL.revokeObjectURL(skin.current.skinUrl)
skin.current.capeUrl && URL.revokeObjectURL(skin.current.capeUrl)
switch (redioValue) {
case "skin":
skin.current.skinUrl = nu
skinview3dView.current?.loadSkin(nu, { model: "default" }).then(() =>
skinview3dView.current?.loadSkin(nu, { model: "default" })
)
break
case "slim":
skin.current.skinUrl = nu
skinview3dView.current?.loadSkin(nu, { model: "slim" }).then(() =>
skinview3dView.current?.loadSkin(nu, { model: "slim" })
)
break
case "cape":
skin.current.capeUrl = nu
skinview3dView.current?.loadCape(nu).then(() => {
skinview3dView.current?.loadCape(nu)
})
}
}
}, [file, redioValue])
useEffect(() => {
skinview3dView.current?.render()
return skinview3dView.current?.dispose
}, [])
const onRadioChange = (_a: React.ChangeEvent<HTMLInputElement>, value: string) => {
setRedioValue(value)
}
const handleChange = (newFile: File | null) => {
setFile(newFile)
}
useTilg()
return (<>
<Box sx={{
display: "grid", gap: "1em", gridTemplateAreas: {
lg: '"a b" ". b"',
xs: '"a" "b"'
}, gridTemplateColumns: { lg: "1fr 1fr" }
}}>
<Card sx={{ gridArea: "a" }}>
<CardHeader title="设置皮肤" />
<CardContent>
<FormControl>
<FormLabel></FormLabel>
<RadioGroup
row
onChange={onRadioChange}
value={redioValue}
>
<FormControlLabel value="skin" control={<Radio />} label="Steve" />
<FormControlLabel value="slim" control={<Radio />} label="Alex" />
<FormControlLabel value="cape" control={<Radio />} label="披风" />
</RadioGroup>
<br />
<MuiFileInput label="选择文件" value={file} inputProps={{ accept: 'image/png' }} onChange={handleChange} />
</FormControl>
</CardContent>
<CardActions>
<Button variant="contained" sx={{ maxWidth: "3em" }}></Button>
</CardActions>
</Card>
<Card sx={{ gridArea: "b" }}>
<CardHeader title="预览" />
<CardContent>
{file && <ReactSkinview3d
skinUrl={""}
capeUrl={""}
height="250"
width="250"
onReady={v => skinview3dView.current = v.viewer}
/>}
</CardContent>
</Card>
</Box>
</>)
}
export default Textures

View File

@ -64,7 +64,7 @@ func Auth(ctx context.Context, t yggdrasil.ValidateToken, client *ent.Client, c
ut, err := client.UserToken.Query().Where(usertoken.HasUserWith(user.ID(claims.UID))).First(ctx)
if err != nil {
var ne *ent.NotFoundError
if !errors.As(err, &ne) {
if errors.As(err, &ne) {
return 0, errors.Join(err, ErrTokenInvalid)
}
return 0, err