用户管理

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 {
uid: number
uuid: number
uuid: string
is_admin: boolean
is_disable: boolean
email: string
reg_ip: string
name: string
@ -63,4 +64,6 @@ export interface EditUser {
name: string
password: string
is_admin: boolean
is_disable: boolean
del_textures: boolean
}

View File

@ -5,7 +5,7 @@ export default function Loading() {
return (
<>
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.modal + 1 }}
open={true}
>
<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 SettingsIcon from '@mui/icons-material/Settings';
import { Link } from "react-router-dom";
import GroupIcon from '@mui/icons-material/Group';
const drawerWidth = 240;
const DrawerOpen = atom(false)
@ -242,8 +242,9 @@ const MyDrawer = function MyDrawer() {
const adminDrawerList = React.useMemo(() => [
{
icon: <PersonIcon />,
title: 'test'
icon: <GroupIcon />,
title: '用户管理',
link: '/admin/user'
}
] as ListItem[], [])

View File

@ -6,8 +6,8 @@ import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import useTitle from '@/hooks/useTitle';
import { useRequest } from 'ahooks';
import { ListUser } from '@/apis/apis';
import { useRequest } from 'ahooks';
import { ListUser, editUser } from '@/apis/apis';
import { useEffect, useState } from 'react';
import { useAtomValue } from 'jotai';
import { token } from '@/store/store';
@ -22,8 +22,13 @@ import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import { UserInfo } from '@/apis/model';
import { EditUser, UserInfo } from '@/apis/model';
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() {
useTitle("用户管理")
@ -102,7 +107,7 @@ export default function UserAdmin() {
<Alert onClose={() => setErr("")} severity="error">{err}</Alert>
</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
setOpen: (b: boolean) => void
row: UserInfo | null
onUpdate: () => void
}
function MyDialog({ open, row, setOpen }: MyDialogProp) {
function MyDialog({ open, row, setOpen, onUpdate }: MyDialogProp) {
const handleClose = () => {
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(() => {
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])
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}>
<DialogTitle></DialogTitle>
<DialogContent>
<TextField
margin="dense"
label="邮箱"
type="email"
fullWidth
variant="standard"
value={nrow?.email}
onChange={e => setNrow(produce(v => {
if (!v) return
v.email = e.target.value
return
}))}
/>
<DialogContent sx={{
display: "grid", gap: '1em', gridTemplateColumns: {
md: "auto 175px",
xs: "1fr"
}
}}>
<Box sx={{ display: "flex", flexDirection: 'column', gap: '0.5em' }}>
<TextField
margin="dense"
label="邮箱"
type="email"
variant="standard"
value={erow?.email}
onChange={e => setErow(produce(v => {
v.email = e.target.value
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>
<DialogActions>
<Button onClick={handleClose}></Button>
<Button onClick={handleOpen}></Button>
</DialogActions>
</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 Typography from '@mui/material/Typography';
import CardHeader from '@mui/material/CardHeader';
import { useHover, useMemoizedFn, useRequest, useUnmount } from 'ahooks';
import { LayoutAlertErr, user } from '@/store/store';
import { useAtomValue, useSetAtom } from 'jotai';
import { yggProfile } from '@/apis/apis';
import { user } from '@/store/store';
import { useAtomValue } from 'jotai';
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 '@/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 SkinViewUUID from '@/components/SkinViewUUID';
const Profile = function Profile() {
const navigate = useNavigate();
const setErr = useSetAtom(LayoutAlertErr)
const userinfo = useAtomValue(user)
const [textures, setTextures] = useState({ skin: "", cape: "", model: "default" })
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 (
<>
<Box sx={{
@ -69,20 +41,7 @@ const Profile = function Profile() {
<Card sx={{ gridArea: "b" }}>
<CardHeader title="皮肤" />
<CardContent sx={{ display: "flex", justifyContent: 'center' }}>
{
(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>
}
<SkinViewUUID uuid={userinfo?.uuid ?? ""} width={250} height={250} />
</CardContent>
<CardActions>
<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() {
const u = new URL((import.meta.env.VITE_APIADDR ?? location.origin) + "/api/yggdrasil")
return u.toString()

View File

@ -48,9 +48,10 @@ type ChangePasswd struct {
type UserList struct {
UserInfo
Email string `json:"email"`
RegIp string `json:"reg_ip"`
Name string `json:"name"`
Email string `json:"email"`
RegIp string `json:"reg_ip"`
Name string `json:"name"`
IsDisable bool `json:"is_disable"`
}
type ChangeName struct {

View File

@ -63,9 +63,10 @@ func (w *WebService) ListUser(ctx context.Context, page int, email, name string)
UUID: v.Edges.Profile.UUID,
IsAdmin: utilsService.IsAdmin(v.State),
},
Email: v.Email,
RegIp: v.RegIP,
Name: v.Edges.Profile.Name,
Email: v.Email,
RegIp: v.RegIP,
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 {
uuid := ""
err := utils.WithTx(ctx, w.client, func(tx *ent.Tx) error {
up := tx.User.UpdateOneID(uid).SetEmail(u.Email)
if u.Password != "" {
@ -93,13 +95,14 @@ func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) er
}
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 {
return err
}
uuid = userProfile.UUID
tl := []string{"skin", "cape"}
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 {
return err
}
@ -122,9 +125,14 @@ func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) er
return nil
})
if err != nil {
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
}