修改皮肤预览库
This commit is contained in:
parent
f86c8e4f80
commit
d39d406391
@ -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"
|
||||
},
|
||||
|
15
frontend/pnpm-lock.yaml
generated
15
frontend/pnpm-lock.yaml
generated
@ -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:
|
||||
|
@ -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() {
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
|
||||
<Route path="/profile" element={<NeedLogin><Profile /></NeedLogin>} />
|
||||
<Route path="/textures" element={<NeedLogin><Textures /></NeedLogin>} />
|
||||
<Route path="/security" element={<NeedLogin><Security /></NeedLogin>} />
|
||||
<Route element={<NeedLogin><Outlet /></NeedLogin>}>
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/textures" element={<Textures />} />
|
||||
<Route path="/security" element={<Security />} />
|
||||
</Route>
|
||||
|
||||
<Route path="admin" element={<NeedLogin needAdmin><Outlet /></NeedLogin>}>
|
||||
<Route path="user" element={<UserAdmin />} />
|
||||
</Route>
|
||||
|
||||
</Route>
|
||||
</Routes>
|
||||
<ScrollRestoration />
|
||||
@ -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}</>
|
||||
}
|
101
frontend/src/components/Skinview3d.tsx
Normal file
101
frontend/src/components/Skinview3d.tsx
Normal file
@ -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<HTMLCanvasElement | null>(null);
|
||||
const skinviewRef = useRef<SkinViewer>();
|
||||
|
||||
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 <canvas className={className} ref={canvasRef} />;
|
||||
})
|
||||
|
||||
export default ReactSkinview3d;
|
@ -46,6 +46,7 @@ export default function SignUp() {
|
||||
}
|
||||
if (captchaToken == "") {
|
||||
setRegErr("验证码无效")
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
register(d.email ?? "", d.username ?? "", d.password ?? "", captchaToken).
|
||||
|
@ -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 (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Dessert (100g serving)</TableCell>
|
||||
<TableCell align="right">Calories</TableCell>
|
||||
<TableCell align="right">Fat (g)</TableCell>
|
||||
<TableCell align="right">Carbs (g)</TableCell>
|
||||
<TableCell align="right">Protein (g)</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.name}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell component="th" scope="row">
|
||||
{row.name}
|
||||
</TableCell>
|
||||
<TableCell align="right">{row.calories}</TableCell>
|
||||
<TableCell align="right">{row.fat}</TableCell>
|
||||
<TableCell align="right">{row.carbs}</TableCell>
|
||||
<TableCell align="right">{row.protein}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
@ -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';
|
||||
@ -71,14 +71,17 @@ const Profile = function Profile() {
|
||||
<CardContent sx={{ display: "flex", justifyContent: 'center' }}>
|
||||
{
|
||||
(SkinInfo.loading && !SkinInfo.data) ? <Skeleton variant="rectangular" width={250} height={250} />
|
||||
: (textures.skin != "" || textures.cape != "") && (
|
||||
: (textures.skin != "" || textures.cape != "") ? (
|
||||
<MySkin
|
||||
skinUrl={textures.skin}
|
||||
capeUrl={textures.cape}
|
||||
height="250"
|
||||
width="250"
|
||||
options={{ model: textures.model as "default" | "slim" }}
|
||||
/>)
|
||||
/>) : <Box sx={{ minHeight: "250px" }}>
|
||||
<Typography>你还没有设置皮肤</Typography>
|
||||
</Box>
|
||||
|
||||
}
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
@ -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 <div ref={refSkinview3d}>
|
||||
<ReactSkinview3d
|
||||
{...p}
|
||||
onReady={v => [v.viewer.animation = new WalkingAnimation(), v.viewer.autoRotate = true, skinview3dView.current = v.viewer]}
|
||||
onReady={handelOnReady}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
@ -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<File | null>(null)
|
||||
const skin = useRef("")
|
||||
const skinview3dView = useRef<SkinViewer | null>(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,7 +74,7 @@ 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))
|
||||
}
|
||||
|
||||
|
||||
@ -114,15 +111,22 @@ const Textures = function Textures() {
|
||||
<CardHeader title="预览" />
|
||||
<CardContent>
|
||||
{file && <ReactSkinview3d
|
||||
skinUrl={""}
|
||||
capeUrl={""}
|
||||
skinUrl={skinInfo.skin}
|
||||
capeUrl={skinInfo.cape}
|
||||
height="250"
|
||||
width="250"
|
||||
onReady={v => skinview3dView.current = v.viewer}
|
||||
options={{ model: skinInfo.model as "default" | "slim" }}
|
||||
/>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
<Snackbar
|
||||
open={ok}
|
||||
autoHideDuration={6000}
|
||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
onClose={() => setOk(false)}
|
||||
message="成功"
|
||||
/>
|
||||
{loading && <Loading />}
|
||||
</>)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user