用户管理

This commit is contained in:
xmdhs 2023-10-08 22:58:22 +08:00
parent 4cf7c8aca0
commit 63808f0a14
No known key found for this signature in database
GPG Key ID: E809D6D43DEFCC95
8 changed files with 213 additions and 116 deletions

View File

@ -51,8 +51,9 @@ export interface ApiConfig {
export interface UserInfo { export interface UserInfo {
uid: number uid: number
uuid: number uuid: string
is_admin: boolean is_admin: boolean
is_disable: boolean
email: string email: string
reg_ip: string reg_ip: string
name: string name: string
@ -63,4 +64,6 @@ export interface EditUser {
name: string name: string
password: string password: string
is_admin: boolean is_admin: boolean
is_disable: boolean
del_textures: boolean
} }

View File

@ -5,7 +5,7 @@ export default function Loading() {
return ( return (
<> <>
<Backdrop <Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }} sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.modal + 1 }}
open={true} open={true}
> >
<CircularProgress color="inherit" /> <CircularProgress color="inherit" />

View File

@ -0,0 +1,89 @@
import { yggProfile } from "@/apis/apis";
import { decodeSkin } from "@/utils/skin";
import Skeleton from "@mui/material/Skeleton";
import { useHover, useMemoizedFn, useRequest, useUnmount } from "ahooks";
import { memo, useEffect, useRef, useState } from "react";
import ReactSkinview3d, { ReactSkinview3dOptions } from "@/components/Skinview3d";
import { SkinViewer, WalkingAnimation } from "skinview3d";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
interface prop {
uuid: string
width: number
height: number
}
const SkinViewUUID = memo(function SkinViewUUID({ uuid, width, height }: prop) {
const [textures, setTextures] = useState({ skin: "", cape: "", model: "default" })
const [err, setErr] = useState("")
const SkinInfo = useRequest(() => yggProfile(uuid), {
cacheKey: "/api/yggdrasil/sessionserver/session/minecraft/profile/" + uuid,
onError: e => {
console.warn(e)
setErr(String(e))
},
refreshDeps: [uuid],
})
useEffect(() => {
if (!SkinInfo.data) return
const [skin, cape, model] = decodeSkin(SkinInfo.data)
setTextures({ cape: cape, skin: skin, model: model })
}, [SkinInfo.data])
if (err != "") {
return <Typography color={"error"}>{err}</Typography>
}
return (<>
{
(SkinInfo.loading && !SkinInfo.data) ? <Skeleton variant="rectangular" width={width} height={height} />
: (textures.skin != "" || textures.cape != "") ? (
<MySkin
skinUrl={textures.skin}
capeUrl={textures.cape}
height={width}
width={height}
options={{ model: textures.model as "default" | "slim" }}
/>) : <Box sx={{ minHeight: height + "px" }}>
<Typography></Typography>
</Box>
}
</>)
})
const MySkin = function MySkin(p: ReactSkinview3dOptions) {
const refSkinview3d = useRef(null);
const skinisHovering = useHover(refSkinview3d);
const skinview3dView = useRef<SkinViewer | null>(null);
useEffect(() => {
if (skinview3dView.current) {
skinview3dView.current.autoRotate = !skinisHovering
}
if (skinview3dView.current?.animation) {
skinview3dView.current.animation.paused = skinisHovering
}
}, [skinisHovering])
useUnmount(() => {
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={handelOnReady}
/>
</div>
}
export default SkinViewUUID

View File

@ -34,7 +34,7 @@ import PersonIcon from '@mui/icons-material/Person';
import SecurityIcon from '@mui/icons-material/Security'; import SecurityIcon from '@mui/icons-material/Security';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import GroupIcon from '@mui/icons-material/Group';
const drawerWidth = 240; const drawerWidth = 240;
const DrawerOpen = atom(false) const DrawerOpen = atom(false)
@ -242,8 +242,9 @@ const MyDrawer = function MyDrawer() {
const adminDrawerList = React.useMemo(() => [ const adminDrawerList = React.useMemo(() => [
{ {
icon: <PersonIcon />, icon: <GroupIcon />,
title: 'test' title: '用户管理',
link: '/admin/user'
} }
] as ListItem[], []) ] as ListItem[], [])

View File

@ -7,7 +7,7 @@ import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import useTitle from '@/hooks/useTitle'; import useTitle from '@/hooks/useTitle';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import { ListUser } from '@/apis/apis'; import { ListUser, editUser } from '@/apis/apis';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { token } from '@/store/store'; import { token } from '@/store/store';
@ -22,8 +22,13 @@ import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions'; import DialogActions from '@mui/material/DialogActions';
import { UserInfo } from '@/apis/model'; import { EditUser, UserInfo } from '@/apis/model';
import { produce } from 'immer' import { produce } from 'immer'
import Checkbox from '@mui/material/Checkbox';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import SkinViewUUID from '@/components/SkinViewUUID';
import Loading from '@/components/Loading';
export default function UserAdmin() { export default function UserAdmin() {
useTitle("用户管理") useTitle("用户管理")
@ -102,7 +107,7 @@ export default function UserAdmin() {
<Alert onClose={() => setErr("")} severity="error">{err}</Alert> <Alert onClose={() => setErr("")} severity="error">{err}</Alert>
</Snackbar> </Snackbar>
<MyDialog open={open} setOpen={setOpen} row={row} /> <MyDialog open={open} setOpen={setOpen} row={row} onUpdate={() => run(page, nowtoken, email, name)} />
</>); </>);
} }
@ -110,45 +115,109 @@ interface MyDialogProp {
open: boolean open: boolean
setOpen: (b: boolean) => void setOpen: (b: boolean) => void
row: UserInfo | null row: UserInfo | null
onUpdate: () => void
} }
function MyDialog({ open, row, setOpen }: MyDialogProp) { function MyDialog({ open, row, setOpen, onUpdate }: MyDialogProp) {
const handleClose = () => { const handleClose = () => {
setOpen(false) setOpen(false)
} }
const [nrow, setNrow] = useState(row) const [erow, setErow] = useState<EditUser>({
email: "",
name: "",
password: "",
is_admin: false,
is_disable: false,
del_textures: false,
})
const [load, setLoad] = useState(false)
const nowToken = useAtomValue(token)
const [err, setErr] = useState("")
useEffect(() => { useEffect(() => {
setNrow(row) setErow({
email: row?.email ?? "",
name: row?.name ?? "",
password: "",
is_admin: row?.is_admin ?? false,
is_disable: row?.is_disable ?? false,
del_textures: false,
})
}, [row]) }, [row])
const handleOpen = () => { const handleOpen = () => {
if (load) return
setLoad(true)
editUser(erow, nowToken, String(row?.uid)).then(() => [setOpen(false), onUpdate()]).finally(() => setLoad(false)).
catch(e => setErr(String(e)))
} }
return ( return (<>
<Dialog open={open}> <Dialog open={open}>
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
<DialogContent> <DialogContent sx={{
display: "grid", gap: '1em', gridTemplateColumns: {
md: "auto 175px",
xs: "1fr"
}
}}>
<Box sx={{ display: "flex", flexDirection: 'column', gap: '0.5em' }}>
<TextField <TextField
margin="dense" margin="dense"
label="邮箱" label="邮箱"
type="email" type="email"
fullWidth
variant="standard" variant="standard"
value={nrow?.email} value={erow?.email}
onChange={e => setNrow(produce(v => { onChange={e => setErow(produce(v => {
if (!v) return
v.email = e.target.value v.email = e.target.value
return return
}))} }))}
/> />
<TextField
margin="dense"
label="用户名"
type="text"
variant="standard"
value={erow?.name}
onChange={e => setErow(produce(v => {
v.name = e.target.value
return
}))}
/>
<TextField
margin="dense"
label="密码"
type="text"
variant="standard"
value={erow?.password}
onChange={e => setErow(produce(v => {
v.password = e.target.value
return
}))}
/>
<FormGroup>
<FormControlLabel control={<Checkbox checked={erow?.is_admin} onChange={e => setErow(produce(v => {
v.is_admin = e.target.checked
}))} />} label="管理权限" />
<FormControlLabel control={<Checkbox checked={erow?.is_disable} onChange={e => setErow(produce(v => {
v.is_disable = e.target.checked
}))} />} label="禁用" />
<FormControlLabel control={<Checkbox checked={erow?.del_textures} onChange={e => setErow(produce(v => {
v.del_textures = e.target.checked
}))} />} label="清空材质" />
</FormGroup>
</Box>
<SkinViewUUID uuid={row?.uuid ?? ""} width={175} height={175} />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose}></Button> <Button onClick={handleClose}></Button>
<Button onClick={handleOpen}></Button> <Button onClick={handleOpen}></Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
) {load && <Loading />}
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={err != ""} >
<Alert onClose={() => setErr("")} severity="error">{err}</Alert>
</Snackbar>
</>)
} }

View File

@ -4,48 +4,20 @@ import CardContent from '@mui/material/CardContent';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import CardHeader from '@mui/material/CardHeader'; import CardHeader from '@mui/material/CardHeader';
import { useHover, useMemoizedFn, useRequest, useUnmount } from 'ahooks'; import { user } from '@/store/store';
import { LayoutAlertErr, user } from '@/store/store'; import { useAtomValue } from 'jotai';
import { useAtomValue, useSetAtom } from 'jotai';
import { yggProfile } from '@/apis/apis';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { useEffect, useRef, useState } from 'react';
import { decodeSkin } from '@/utils/skin';
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';
import useTitle from '@/hooks/useTitle'; import useTitle from '@/hooks/useTitle';
import SkinViewUUID from '@/components/SkinViewUUID';
const Profile = function Profile() { const Profile = function Profile() {
const navigate = useNavigate(); const navigate = useNavigate();
const setErr = useSetAtom(LayoutAlertErr)
const userinfo = useAtomValue(user) const userinfo = useAtomValue(user)
const [textures, setTextures] = useState({ skin: "", cape: "", model: "default" })
useTitle("个人信息") useTitle("个人信息")
const SkinInfo = useRequest(() => yggProfile(userinfo.uuid ?? ""), {
cacheKey: "/api/yggdrasil/sessionserver/session/minecraft/profile/" + userinfo?.uuid,
onError: e => {
console.warn(e)
setErr(String(e))
},
refreshDeps: [userinfo?.uuid],
})
useEffect(() => {
if (!SkinInfo.data) return
const [skin, cape, model] = decodeSkin(SkinInfo.data)
setTextures({ cape: cape, skin: skin, model: model })
}, [SkinInfo.data])
return ( return (
<> <>
<Box sx={{ <Box sx={{
@ -69,20 +41,7 @@ const Profile = function Profile() {
<Card sx={{ gridArea: "b" }}> <Card sx={{ gridArea: "b" }}>
<CardHeader title="皮肤" /> <CardHeader title="皮肤" />
<CardContent sx={{ display: "flex", justifyContent: 'center' }}> <CardContent sx={{ display: "flex", justifyContent: 'center' }}>
{ <SkinViewUUID uuid={userinfo?.uuid ?? ""} width={250} height={250} />
(SkinInfo.loading && !SkinInfo.data) ? <Skeleton variant="rectangular" width={250} height={250} />
: (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> </CardContent>
<CardActions> <CardActions>
<Button onClick={() => navigate('/textures')} size="small"></Button> <Button onClick={() => navigate('/textures')} size="small"></Button>
@ -101,39 +60,6 @@ const Profile = function Profile() {
) )
} }
const MySkin = function MySkin(p: ReactSkinview3dOptions) {
const refSkinview3d = useRef(null);
const skinisHovering = useHover(refSkinview3d);
const skinview3dView = useRef<SkinViewer | null>(null);
useEffect(() => {
if (skinview3dView.current) {
skinview3dView.current.autoRotate = !skinisHovering
}
if (skinview3dView.current?.animation) {
skinview3dView.current.animation.paused = skinisHovering
}
}, [skinisHovering])
useUnmount(() => {
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={handelOnReady}
/>
</div>
}
function getYggRoot() { function getYggRoot() {
const u = new URL((import.meta.env.VITE_APIADDR ?? location.origin) + "/api/yggdrasil") const u = new URL((import.meta.env.VITE_APIADDR ?? location.origin) + "/api/yggdrasil")
return u.toString() return u.toString()

View File

@ -51,6 +51,7 @@ type UserList struct {
Email string `json:"email"` Email string `json:"email"`
RegIp string `json:"reg_ip"` RegIp string `json:"reg_ip"`
Name string `json:"name"` Name string `json:"name"`
IsDisable bool `json:"is_disable"`
} }
type ChangeName struct { type ChangeName struct {

View File

@ -66,6 +66,7 @@ func (w *WebService) ListUser(ctx context.Context, page int, email, name string)
Email: v.Email, Email: v.Email,
RegIp: v.RegIP, RegIp: v.RegIP,
Name: v.Edges.Profile.Name, Name: v.Edges.Profile.Name,
IsDisable: utilsService.IsDisable(v.State),
}) })
} }
@ -77,6 +78,7 @@ func (w *WebService) ListUser(ctx context.Context, page int, email, name string)
} }
func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) error { func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) error {
uuid := ""
err := utils.WithTx(ctx, w.client, func(tx *ent.Tx) error { err := utils.WithTx(ctx, w.client, func(tx *ent.Tx) error {
up := tx.User.UpdateOneID(uid).SetEmail(u.Email) up := tx.User.UpdateOneID(uid).SetEmail(u.Email)
if u.Password != "" { if u.Password != "" {
@ -93,13 +95,14 @@ func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) er
} }
if u.DelTextures { if u.DelTextures {
up, err := tx.UserProfile.Query().Where(userprofile.ID(uid)).First(ctx) userProfile, err := tx.UserProfile.Query().Where(userprofile.ID(uid)).First(ctx)
if err != nil { if err != nil {
return err return err
} }
uuid = userProfile.UUID
tl := []string{"skin", "cape"} tl := []string{"skin", "cape"}
for _, v := range tl { for _, v := range tl {
err := utilsService.DelTexture(ctx, up.ID, v, w.client, w.config) err := utilsService.DelTexture(ctx, userProfile.ID, v, w.client, w.config)
if err != nil { if err != nil {
return err return err
} }
@ -122,9 +125,14 @@ func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) er
return nil return nil
}) })
if err != nil { if err != nil {
return fmt.Errorf("EditUser: %w", err) return fmt.Errorf("EditUser: %w", err)
} }
if uuid != "" {
err = w.cache.Del([]byte("Profile" + uuid))
if err != nil {
return fmt.Errorf("EditUser: %w", err)
}
}
return nil return nil
} }