diff --git a/frontend/package.json b/frontend/package.json index 768895e..629821e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,6 @@ "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" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 486ee08..1bad291 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -41,9 +41,6 @@ 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 @@ -2020,18 +2017,6 @@ 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: diff --git a/frontend/src/Route.tsx b/frontend/src/Route.tsx index 1967338..1c6772b 100644 --- a/frontend/src/Route.tsx +++ b/frontend/src/Route.tsx @@ -1,4 +1,4 @@ -import { Routes, Route, createBrowserRouter, RouterProvider, useNavigate } from "react-router-dom"; +import { Routes, Route, createBrowserRouter, RouterProvider, useNavigate, Outlet } from "react-router-dom"; import { ScrollRestoration } from "react-router-dom"; import Login from '@/views/Login' import Register from '@/views/Register' @@ -11,6 +11,7 @@ import { token } from "@/store/store"; import { ApiErr } from "@/apis/error"; import { userInfo } from "@/apis/apis"; import { useRequest } from "ahooks"; +import UserAdmin from "@/views/admin/UserAdmin"; const router = createBrowserRouter([ { path: "*", Component: Root }, @@ -26,9 +27,16 @@ function Root() { } /> } /> - } /> - } /> - } /> + }> + } /> + } /> + } /> + + + }> + } /> + + @@ -46,10 +54,10 @@ export function PageRoute() { } -function NeedLogin({ children }: { children: JSX.Element }) { +function NeedLogin({ children, needAdmin = false }: { children: JSX.Element, needAdmin?: boolean }) { const t = useAtomValue(token) const navigate = useNavigate(); - useRequest(() => userInfo(t), { + const { data, loading } = useRequest(() => userInfo(t), { refreshDeps: [t], cacheKey: "/api/v1/user" + t, staleTime: 60000, @@ -62,7 +70,11 @@ function NeedLogin({ children }: { children: JSX.Element }) { }) if (t == "") { navigate("/login") - return + return <> + } + if (!loading && data && needAdmin && !data.is_admin) { + navigate("/login") + return <> } return <> {children} } \ No newline at end of file diff --git a/frontend/src/components/Skinview3d.tsx b/frontend/src/components/Skinview3d.tsx new file mode 100644 index 0000000..b4fdcc8 --- /dev/null +++ b/frontend/src/components/Skinview3d.tsx @@ -0,0 +1,101 @@ +import { memo, useEffect, useRef } from "react"; +import { SkinViewer, SkinViewerOptions } from "skinview3d"; + +// https://github.com/Hacksore/react-skinview3d/blob/master/src/index.tsx + +/** + * This is the interface that describes the parameter in `onReady` + */ +export interface ViewerReadyCallbackOptions { + /** + * The instance of the skinview3d + */ + viewer: SkinViewer; + /** + * The ref to the canvas element + */ + canvasRef: HTMLCanvasElement; +} + +export interface ReactSkinview3dOptions { + /** + * The class names to apply to the canvas + */ + className?: string; + /** + * The width of the canvas + */ + width: number | string; + /** + * The height of the canvas + */ + height: number | string; + /** + * The skin to load in the canvas + */ + skinUrl: string; + /** + * The cape to load in the canvas + */ + capeUrl?: string; + /** + * A function that is called when the skin viewer is ready + * @param {SkinViewer} instance callback function to execute when the viewer is loaded {@link SkinViewer} + * @example + * onReady((instance) => { + * console.log(instance) + * }) + */ + onReady?: ({ viewer, canvasRef }: ViewerReadyCallbackOptions) => void; + /** + * Parameters passed to the skinview3d constructor allowing you to override or add extra features + * @notes please take a look at the upstream repo for more info + * [bs-community/skinview3d](https://bs-community.github.io/skinview3d/) + */ + options?: SkinViewerOptions; +} + +/** + * A skinview3d component + */ +const ReactSkinview3d = memo(function ReactSkinview3d({ + className, + width, + height, + skinUrl, + capeUrl, + onReady, + options, +}: ReactSkinview3dOptions) { + const canvasRef = useRef(null); + const skinviewRef = useRef(); + + useEffect(() => { + if (!canvasRef.current) return + + const viewer = new SkinViewer({ + canvas: canvasRef.current, + width: Number(width), + height: Number(height), + ...options, + }); + + // handle cape/skin load initially + skinUrl && viewer.loadSkin(skinUrl, { model: options?.model ?? "auto-detect" }); + capeUrl && viewer.loadCape(capeUrl); + + skinviewRef.current = viewer; + + // call onReady with the viewer instance + if (onReady) { + onReady({ viewer: skinviewRef.current, canvasRef: canvasRef.current }); + } + + return () => viewer.dispose() + }, [capeUrl, height, onReady, options, skinUrl, width]); + + + return ; +}) + +export default ReactSkinview3d; \ No newline at end of file diff --git a/frontend/src/views/Register.tsx b/frontend/src/views/Register.tsx index 66a2c05..c440331 100644 --- a/frontend/src/views/Register.tsx +++ b/frontend/src/views/Register.tsx @@ -46,6 +46,7 @@ export default function SignUp() { } if (captchaToken == "") { setRegErr("验证码无效") + return } setLoading(true) register(d.email ?? "", d.username ?? "", d.password ?? "", captchaToken). diff --git a/frontend/src/views/admin/UserAdmin.tsx b/frontend/src/views/admin/UserAdmin.tsx index 921ff72..4265e82 100644 --- a/frontend/src/views/admin/UserAdmin.tsx +++ b/frontend/src/views/admin/UserAdmin.tsx @@ -1,5 +1,63 @@ +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import useTitle from '@/hooks/useTitle'; + +function createData( + name: string, + calories: number, + fat: number, + carbs: number, + protein: number, +) { + return { name, calories, fat, carbs, protein }; +} + +const rows = [ + createData('Frozen yoghurt', 159, 6.0, 24, 4.0), + createData('Ice cream sandwich', 237, 9.0, 37, 4.3), + createData('Eclair', 262, 16.0, 24, 6.0), + createData('Cupcake', 305, 3.7, 67, 4.3), + createData('Gingerbread', 356, 16.0, 49, 3.9), +]; export default function UserAdmin() { + useTitle("用户管理") - return <> + + return ( + + + + + Dessert (100g serving) + Calories + Fat (g) + Carbs (g) + Protein (g) + + + + {rows.map((row) => ( + + + {row.name} + + {row.calories} + {row.fat} + {row.carbs} + {row.protein} + + ))} + +
+
+ ); } \ No newline at end of file diff --git a/frontend/src/views/profile/Profile.tsx b/frontend/src/views/profile/Profile.tsx index 242791f..0cdaf32 100644 --- a/frontend/src/views/profile/Profile.tsx +++ b/frontend/src/views/profile/Profile.tsx @@ -4,7 +4,7 @@ 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, useUnmount } from 'ahooks'; +import { useHover, useMemoizedFn, useRequest, useUnmount } from 'ahooks'; import { LayoutAlertErr, user } from '@/store/store'; import { useAtomValue, useSetAtom } from 'jotai'; import { yggProfile } from '@/apis/apis'; @@ -12,8 +12,8 @@ import { useNavigate } from 'react-router-dom'; import Box from '@mui/material/Box'; import { useEffect, useRef, useState } from 'react'; import { decodeSkin } from '@/utils/skin'; -import ReactSkinview3d from "react-skinview3d" -import type { ReactSkinview3dOptions } from "react-skinview3d" +import ReactSkinview3d from '@/components/Skinview3d' +import type { ReactSkinview3dOptions } from '@/components/Skinview3d' import { WalkingAnimation } from "skinview3d" import type { SkinViewer } from "skinview3d" import Skeleton from '@mui/material/Skeleton'; @@ -44,7 +44,7 @@ const Profile = function Profile() { }, [SkinInfo.data]) - + return ( <> @@ -71,14 +71,17 @@ const Profile = function Profile() { { (SkinInfo.loading && !SkinInfo.data) ? - : (textures.skin != "" || textures.cape != "") && ( + : (textures.skin != "" || textures.cape != "") ? ( ) + />) : + 你还没有设置皮肤 + + } @@ -117,10 +120,16 @@ const MySkin = function MySkin(p: ReactSkinview3dOptions) { skinview3dView.current?.dispose() }) + const handelOnReady = useMemoizedFn(v => { + v.viewer.animation = new WalkingAnimation() + v.viewer.autoRotate = true + skinview3dView.current = v.viewer + }) + return
[v.viewer.animation = new WalkingAnimation(), v.viewer.autoRotate = true, skinview3dView.current = v.viewer]} + onReady={handelOnReady} />
} diff --git a/frontend/src/views/profile/Textures.tsx b/frontend/src/views/profile/Textures.tsx index 6f42a7c..93db294 100644 --- a/frontend/src/views/profile/Textures.tsx +++ b/frontend/src/views/profile/Textures.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import FormControl from "@mui/material/FormControl"; @@ -11,54 +11,51 @@ import { CardHeader } from "@mui/material"; import useTitle from "@/hooks/useTitle"; import { MuiFileInput } from 'mui-file-input' import Box from "@mui/material/Box"; -import ReactSkinview3d from "react-skinview3d"; +import ReactSkinview3d from '@/components/Skinview3d' import { useUnmount } from "ahooks"; -import { SkinViewer } from "skinview3d"; import { useAtomValue, useSetAtom } from "jotai"; import { LayoutAlertErr, token, user } from "@/store/store"; import { upTextures } from "@/apis/apis"; import Loading from "@/components/Loading"; +import Snackbar from "@mui/material/Snackbar"; const Textures = function Textures() { const [redioValue, setRedioValue] = useState("skin") useTitle("上传皮肤") const [file, setFile] = useState(null) - const skin = useRef("") - const skinview3dView = useRef(null); const setErr = useSetAtom(LayoutAlertErr) const [loading, setLoading] = useState(false) const userinfo = useAtomValue(user) const nowToken = useAtomValue(token) + const [ok, setOk] = useState(false) + const [skinInfo, setSkinInfo] = useState({ + skin: "", + cape: "", + model: "default" + }) useUnmount(() => { - skin.current && URL.revokeObjectURL(skin.current) - skinview3dView.current?.dispose() + skinInfo.skin && URL.revokeObjectURL(skinInfo.skin) + skinInfo.cape && URL.revokeObjectURL(skinInfo.cape) }) useEffect(() => { if (file) { + setSkinInfo(v => { + URL.revokeObjectURL(v.skin); + URL.revokeObjectURL(v.cape); + return { skin: "", cape: "", model: "" } + }) const nu = URL.createObjectURL(file) - skin.current && URL.revokeObjectURL(skin.current) - skinview3dView.current?.loadSkin(null) - skinview3dView.current?.loadCape(null) switch (redioValue) { case "skin": - skin.current = nu - skinview3dView.current?.loadSkin(nu, { model: "default" }).then(() => - skinview3dView.current?.loadSkin(nu, { model: "default" }) - ) + setSkinInfo({ skin: nu, cape: "", model: "default" }) break case "slim": - skin.current = nu - skinview3dView.current?.loadSkin(nu, { model: "slim" }).then(() => - skinview3dView.current?.loadSkin(nu, { model: "slim" }) - ) + setSkinInfo({ skin: nu, cape: "", model: "slim" }) break case "cape": - skin.current = nu - skinview3dView.current?.loadCape(nu).then(() => { - skinview3dView.current?.loadCape(nu) - }) + setSkinInfo({ skin: "", cape: nu, model: "slim" }) } } }, [file, redioValue]) @@ -77,10 +74,10 @@ const Textures = function Textures() { const textureType = redioValue == "cape" ? "cape" : "skin" const model = redioValue == "slim" ? "slim" : "" upTextures(userinfo.uuid, nowToken, textureType, model, file).catch(e => [setErr(String(e)), console.warn(e)]). - finally(() => setLoading(false)) + finally(() => setLoading(false)).then(() => setOk(true)) } - + return (<> {file && skinview3dView.current = v.viewer} + options={{ model: skinInfo.model as "default" | "slim" }} />} + setOk(false)} + message="成功" + /> {loading && } ) }