TinySkin/frontend/src/views/Layout.tsx

291 lines
9.2 KiB
TypeScript

import * as React from 'react';
import { styled, useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Drawer from '@mui/material/Drawer';
import Toolbar from '@mui/material/Toolbar';
import List from '@mui/material/List';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import AppBar from '@mui/material/AppBar';
import { Outlet } from 'react-router-dom';
import { AccountCircle } from '@mui/icons-material';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { LayoutAlertErr, token, user } from '@/store/store';
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import Button from '@mui/material/Button';
import { useNavigate } from "react-router-dom";
import { useRequest, useMemoizedFn } from 'ahooks';
import { getConfig, userInfo } from '@/apis/apis'
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
import { memo } from 'react';
import useMediaQuery from '@mui/material/useMediaQuery';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
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';
import { ApiErr } from '@/apis/error';
const drawerWidth = 240;
const DrawerOpen = atom(false)
const DrawerHeader = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
justifyContent: 'flex-end',
}));
interface ListItem {
icon: JSX.Element
title: string
link: string
}
const Layout = memo(function Layout() {
const theme = useTheme();
const [err, setErr] = useAtom(LayoutAlertErr)
return (<>
<Box sx={{ display: 'flex' }}>
<AppBar position="fixed"
sx={{
zIndex: { lg: theme.zIndex.drawer + 1 }
}}
>
<MyToolbar />
</AppBar>
<MyDrawer />
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={err != ""} >
<Alert onClose={() => setErr("")} severity="error">{err}</Alert>
</Snackbar>
<Box
component="main"
sx={{
flexGrow: 1, bgcolor: 'background.default', p: 3
}}
>
<Toolbar />
<Container maxWidth="lg">
<Outlet />
</Container>
</Box>
</Box>
</>)
})
const MyToolbar = memo(function MyToolbar() {
const [nowUser, setNowUser] = useAtom(user)
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const navigate = useNavigate();
const setToken = useSetAtom(token)
const setErr = useSetAtom(LayoutAlertErr)
const setOpen = useSetAtom(DrawerOpen)
const server = useRequest(getConfig, {
cacheKey: "/api/v1/config",
staleTime: 60000,
onError: e => {
console.warn(e)
setErr(String(e))
}
})
const handleLogOut = useMemoizedFn(() => {
setAnchorEl(null);
setNowUser({ name: "", uuid: "" })
setToken("")
navigate("/login")
})
return (
<>
<Toolbar>
{nowUser.name != "" && (<>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2, display: { lg: 'none' } }}
onClick={() => setOpen(true)}
>
<MenuIcon />
</IconButton >
</>)
}
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
<Link to="/" style={{ color: "unset", textDecoration: "unset" }}>
{server.data?.serverName ?? "皮肤站"}
</Link>
</Typography>
{nowUser.name != "" && (
<div>
<IconButton
size="large"
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={event => setAnchorEl(event.currentTarget)}
color="inherit"
>
<AccountCircle />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
<MenuItem onClick={handleLogOut}></MenuItem>
</Menu>
</div>
)}
{nowUser.name == "" && (
<Button color="inherit" onClick={() => navigate("/login")} ></Button>
)}
</Toolbar>
</>)
})
const MyList = memo(function MyList(p: { list: ListItem[] }) {
return (
<>
<List>
{p.list.map(item =>
<MyListItem {...item} key={item.title} />
)}
</List>
</>
)
})
const MyListItem = function MyListItem(p: ListItem) {
const navigate = useNavigate();
const handleClick = () => {
navigate(p.link)
}
return (
<ListItem disablePadding>
<ListItemButton onClick={handleClick}>
<ListItemIcon>
{p.icon}
</ListItemIcon>
<ListItemText primary={p.title} />
</ListItemButton>
</ListItem>
)
}
const MyDrawer = function MyDrawer() {
const nowToken = useAtomValue(token)
const setErr = useSetAtom(LayoutAlertErr)
const theme = useTheme();
const isLg = useMediaQuery(theme.breakpoints.up('lg'))
const [open, setOpen] = useAtom(DrawerOpen)
const navigate = useNavigate();
const userinfo = useRequest(() => userInfo(nowToken), {
refreshDeps: [nowToken],
cacheKey: "/api/v1/user" + nowToken,
staleTime: 60000,
onError: e => {
if (e instanceof ApiErr && e.code == 5) {
navigate("/login")
}
console.warn(e)
setErr(String(e))
},
})
const userDrawerList = React.useMemo(() => [
{
icon: <PersonIcon />,
title: '个人信息',
link: '/profile'
},
{
icon: <SettingsIcon />,
title: '皮肤设置',
link: '/textures'
},
{
icon: <SecurityIcon />,
title: '账号设置',
link: '/security'
}
] as ListItem[], [])
const adminDrawerList = React.useMemo(() => [
{
icon: <GroupIcon />,
title: '用户管理',
link: '/admin/user'
}
] as ListItem[], [])
return (<>
{userinfo.data && (
<Drawer
sx={{
width: drawerWidth,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: drawerWidth,
boxSizing: 'border-box',
},
}}
variant={isLg ? "persistent" : "temporary"}
anchor="left"
open={open || isLg}
onClose={() => setOpen(false)}
>
<DrawerHeader>
<IconButton onClick={() => setOpen(false)}>
{theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
</IconButton>
</DrawerHeader>
<Divider />
<MyList list={userDrawerList} />
{userinfo.data?.is_admin && (
<>
<Divider />
<MyList list={adminDrawerList} />
</>)}
</Drawer>
)}
</>)
}
export default Layout