From 2363283cc56a02941d5b077ee1d31b925bba897e Mon Sep 17 00:00:00 2001 From: xmdhs Date: Tue, 3 Oct 2023 21:05:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=9A=AE=E8=82=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/index.html | 2 +- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 32 +++++ frontend/src/hooks/useTitle.ts | 16 +++ frontend/src/store/store.ts | 2 +- frontend/src/views/Layout.tsx | 166 +++++++++++++----------- frontend/src/views/Login.tsx | 4 +- frontend/src/views/Register.tsx | 2 + frontend/src/views/profile/Profile.tsx | 23 +++- frontend/src/views/profile/Textures.tsx | 120 ++++++++++++++++- service/utils/auth.go | 2 +- 11 files changed, 280 insertions(+), 90 deletions(-) create mode 100644 frontend/src/hooks/useTitle.ts diff --git a/frontend/index.html b/frontend/index.html index e4b78ea..b3a3de4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + 皮肤站
diff --git a/frontend/package.json b/frontend/package.json index a8d48d2..768895e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1f51ee0..486ee08 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -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: diff --git a/frontend/src/hooks/useTitle.ts b/frontend/src/hooks/useTitle.ts new file mode 100644 index 0000000..6513963 --- /dev/null +++ b/frontend/src/hooks/useTitle.ts @@ -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 + }) +} \ No newline at end of file diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 52c323d..00bc090 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -7,4 +7,4 @@ export const user = atomWithStorage("username", { uuid: "" }) -export const LayoutAlertErr = atom("") +export const LayoutAlertErr = atom("") \ No newline at end of file diff --git a/frontend/src/views/Layout.tsx b/frontend/src/views/Layout.tsx index 79a02c6..31a0353 100644 --- a/frontend/src/views/Layout.tsx +++ b/frontend/src/views/Layout.tsx @@ -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: , - title: '个人信息', - link: '/profile' - }, - { - icon: , - title: '皮肤设置', - link: '/textures' - }, - { - icon: , - title: '安全设置', - link: '/setting' - } - ] as ListItem[], []) - - const adminDrawerList = React.useMemo(() => [ - { - icon: , - title: 'test' - } - ] as ListItem[], []) useTilg() @@ -108,37 +67,9 @@ const Layout = memo(function Layout() { zIndex: { lg: theme.zIndex.drawer + 1 } }} > - + - {userinfo.data && ( - setOpen(false)} - > - - setOpen(false)}> - {theme.direction === 'ltr' ? : } - - - - - {userinfo.data?.is_admin && ( - <> - - - )} - - )} + setErr("")} > setErr("")} severity="error">{err} @@ -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); 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 ( <> @@ -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)} > @@ -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: , + title: '个人信息', + link: '/profile' + }, + { + icon: , + title: '皮肤设置', + link: '/textures' + }, + { + icon: , + title: '安全设置', + link: '/setting' + } + ] as ListItem[], []) + + const adminDrawerList = React.useMemo(() => [ + { + icon: , + title: 'test' + } + ] as ListItem[], []) + + useTilg() + + return (<> + {userinfo.data && ( + setOpen(false)} + > + + setOpen(false)}> + {theme.direction === 'ltr' ? : } + + + + + {userinfo.data?.is_admin && ( + <> + + + )} + + )} + ) +} + export default Layout \ No newline at end of file diff --git a/frontend/src/views/Login.tsx b/frontend/src/views/Login.tsx index 20b276d..03acf69 100644 --- a/frontend/src/views/Login.tsx +++ b/frontend/src/views/Login.tsx @@ -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>(new Map()) const navigate = useNavigate(); + useTitle("登录") const handleSubmit = (event: React.FormEvent) => { 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)) diff --git a/frontend/src/views/Register.tsx b/frontend/src/views/Register.tsx index 7b2b90c..8cf1c3e 100644 --- a/frontend/src/views/Register.tsx +++ b/frontend/src/views/Register.tsx @@ -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(null) const [loading, setLoading] = useState(false); + useTitle("注册") const checkList = React.useRef>(new Map()) diff --git a/frontend/src/views/profile/Profile.tsx b/frontend/src/views/profile/Profile.tsx index 07ca8cf..73c3c3a 100644 --- a/frontend/src/views/profile/Profile.tsx +++ b/frontend/src/views/profile/Profile.tsx @@ -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() { ) -}) +} -const MySkin = memo(function MySkin(p: ReactSkinview3dOptions) { +const MySkin = function MySkin(p: ReactSkinview3dOptions) { const refSkinview3d = useRef(null); const skinisHovering = useHover(refSkinview3d); const skinview3dView = useRef(null); @@ -123,13 +128,17 @@ const MySkin = memo(function MySkin(p: ReactSkinview3dOptions) { } }, [skinisHovering]) + useUnmount(() => { + skinview3dView.current?.dispose() + }) + return
[v.viewer.animation = new WalkingAnimation(), v.viewer.autoRotate = true, skinview3dView.current = v.viewer]} />
-}) +} function getYggRoot() { const u = new URL((import.meta.env.VITE_APIADDR ?? location.origin) + "/api/yggdrasil") diff --git a/frontend/src/views/profile/Textures.tsx b/frontend/src/views/profile/Textures.tsx index 0badeff..24d54e3 100644 --- a/frontend/src/views/profile/Textures.tsx +++ b/frontend/src/views/profile/Textures.tsx @@ -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 = function Textures() { + const [redioValue, setRedioValue] = useState("skin") + useTitle("上传皮肤") + const [file, setFile] = useState(null) + const skin = useRef({ + skinUrl: "", + capeUrl: "", + }) + const skinview3dView = useRef(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, value: string) => { + setRedioValue(value) + } + const handleChange = (newFile: File | null) => { + setFile(newFile) + } + + useTilg() -const Textures = memo(function Textures() { return (<> -

修改皮肤

+ + + + + + 类型 + + } label="Steve" /> + } label="Alex" /> + } label="披风" /> + +
+ +
+
+ + + +
+ + + + {file && skinview3dView.current = v.viewer} + />} + + +
) -}) +} export default Textures \ No newline at end of file diff --git a/service/utils/auth.go b/service/utils/auth.go index ffce23b..e1b3596 100644 --- a/service/utils/auth.go +++ b/service/utils/auth.go @@ -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