用户管理
This commit is contained in:
parent
4cf7c8aca0
commit
63808f0a14
@ -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
|
||||
}
|
@ -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" />
|
||||
|
89
frontend/src/components/SkinViewUUID.tsx
Normal file
89
frontend/src/components/SkinViewUUID.tsx
Normal 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
|
@ -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[], [])
|
||||
|
||||
|
@ -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>
|
||||
</>)
|
||||
}
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user