修改皮肤预览库

This commit is contained in:
xmdhs 2023-10-05 22:27:48 +08:00
parent f86c8e4f80
commit d39d406391
No known key found for this signature in database
GPG Key ID: E809D6D43DEFCC95
8 changed files with 227 additions and 58 deletions

View File

@ -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"
},

View File

@ -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:

View File

@ -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}</>
}

View 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;

View File

@ -46,6 +46,7 @@ export default function SignUp() {
}
if (captchaToken == "") {
setRegErr("验证码无效")
return
}
setLoading(true)
register(d.email ?? "", d.username ?? "", d.password ?? "", captchaToken).

View File

@ -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&nbsp;(g)</TableCell>
<TableCell align="right">Carbs&nbsp;(g)</TableCell>
<TableCell align="right">Protein&nbsp;(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>
);
}

View File

@ -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>
}

View File

@ -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 />}
</>)
}