♻️ 按照 Material Design + Reactjs 重构 #1
| @@ -1,7 +1,7 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"code.smartsheep.studio/hydrogen/identity/pkg/view" | ||||
| 	"code.smartsheep.studio/hydrogen/identity/pkg/views" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"github.com/gofiber/fiber/v2/middleware/cache" | ||||
| 	"github.com/gofiber/fiber/v2/middleware/cors" | ||||
| @@ -92,7 +92,7 @@ func NewServer() { | ||||
| 		Expiration:   24 * time.Hour, | ||||
| 		CacheControl: true, | ||||
| 	}), filesystem.New(filesystem.Config{ | ||||
| 		Root:         http.FS(view.FS), | ||||
| 		Root:         http.FS(views.FS), | ||||
| 		PathPrefix:   "dist", | ||||
| 		Index:        "index.html", | ||||
| 		NotFoundFile: "dist/index.html", | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| @@ -1,136 +0,0 @@ | ||||
| import { | ||||
|   Slide, | ||||
|   Toolbar, | ||||
|   Typography, | ||||
|   AppBar as MuiAppBar, | ||||
|   AppBarProps as MuiAppBarProps, | ||||
|   useScrollTrigger, | ||||
|   IconButton, | ||||
|   styled, | ||||
|   Box, | ||||
|   useMediaQuery, | ||||
| } from "@mui/material"; | ||||
| import { ReactElement, ReactNode, useEffect, useState } from "react"; | ||||
| import { SITE_NAME } from "@/consts"; | ||||
| import { Link } from "react-router-dom"; | ||||
| import NavigationDrawer, { DRAWER_WIDTH, AppNavigationHeader, isMobileQuery } from "@/components/NavigationDrawer"; | ||||
| import MenuIcon from "@mui/icons-material/Menu"; | ||||
|  | ||||
| function HideOnScroll(props: { window?: () => Window; children: ReactElement }) { | ||||
|   const { children, window } = props; | ||||
|   const trigger = useScrollTrigger({ | ||||
|     target: window ? window() : undefined, | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <Slide appear={false} direction="down" in={!trigger}> | ||||
|       {children} | ||||
|     </Slide> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| interface AppBarProps extends MuiAppBarProps { | ||||
|   open?: boolean; | ||||
| } | ||||
|  | ||||
| const ShellAppBar = styled(MuiAppBar, { | ||||
|   shouldForwardProp: (prop) => prop !== "open", | ||||
| })<AppBarProps>(({ theme, open }) => { | ||||
|   const isMobile = useMediaQuery(isMobileQuery); | ||||
|  | ||||
|   return { | ||||
|     transition: theme.transitions.create(["margin", "width"], { | ||||
|       easing: theme.transitions.easing.sharp, | ||||
|       duration: theme.transitions.duration.leavingScreen, | ||||
|     }), | ||||
|     ...(!isMobile && | ||||
|       open && { | ||||
|         width: `calc(100% - ${DRAWER_WIDTH}px)`, | ||||
|         transition: theme.transitions.create(["margin", "width"], { | ||||
|           easing: theme.transitions.easing.easeOut, | ||||
|           duration: theme.transitions.duration.enteringScreen, | ||||
|         }), | ||||
|         marginRight: DRAWER_WIDTH, | ||||
|       }), | ||||
|   }; | ||||
| }); | ||||
|  | ||||
| const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{ | ||||
|   open?: boolean; | ||||
| }>(({ theme, open }) => { | ||||
|   const isMobile = useMediaQuery(isMobileQuery); | ||||
|  | ||||
|   return { | ||||
|     flexGrow: 1, | ||||
|     transition: theme.transitions.create("margin", { | ||||
|       easing: theme.transitions.easing.sharp, | ||||
|       duration: theme.transitions.duration.leavingScreen, | ||||
|     }), | ||||
|     marginRight: -DRAWER_WIDTH, | ||||
|     ...(!isMobile && | ||||
|       open && { | ||||
|         transition: theme.transitions.create("margin", { | ||||
|           easing: theme.transitions.easing.easeOut, | ||||
|           duration: theme.transitions.duration.enteringScreen, | ||||
|         }), | ||||
|         marginRight: 0, | ||||
|       }), | ||||
|     position: "relative", | ||||
|   }; | ||||
| }); | ||||
|  | ||||
| export default function AppShell({ children }: { children: ReactNode }) { | ||||
|   let documentWindow: Window; | ||||
|  | ||||
|   const isMobile = useMediaQuery(isMobileQuery); | ||||
|   const [open, setOpen] = useState(false); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     documentWindow = window; | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <HideOnScroll window={() => documentWindow}> | ||||
|         <ShellAppBar open={open} position="fixed"> | ||||
|           <Toolbar sx={{ height: 64 }}> | ||||
|             <IconButton | ||||
|               size="large" | ||||
|               edge="start" | ||||
|               color="inherit" | ||||
|               aria-label="menu" | ||||
|               sx={{ ml: isMobile ? 0.5 : 0, mr: 2 }} | ||||
|             > | ||||
|               <img src="/favicon.svg" alt="Logo" width={32} height={32} /> | ||||
|             </IconButton> | ||||
|  | ||||
|             <Typography variant="h6" component="div" sx={{ flexGrow: 1, fontSize: "1.2rem" }}> | ||||
|               <Link to="/">{SITE_NAME}</Link> | ||||
|             </Typography> | ||||
|  | ||||
|             <IconButton | ||||
|               size="large" | ||||
|               edge="start" | ||||
|               color="inherit" | ||||
|               aria-label="menu" | ||||
|               onClick={() => setOpen(true)} | ||||
|               sx={{ width: 64, mr: 1, display: !isMobile && open ? "none" : "block" }} | ||||
|             > | ||||
|               <MenuIcon /> | ||||
|             </IconButton> | ||||
|           </Toolbar> | ||||
|         </ShellAppBar> | ||||
|       </HideOnScroll> | ||||
|  | ||||
|       <Box sx={{ display: "flex" }}> | ||||
|         <AppMain open={open}> | ||||
|           <AppNavigationHeader /> | ||||
|  | ||||
|           {children} | ||||
|         </AppMain> | ||||
|  | ||||
|         <NavigationDrawer open={open} onClose={() => setOpen(false)} /> | ||||
|       </Box> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								pkg/views/bun.lockb
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								pkg/views/bun.lockb
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1,4 +1,4 @@ | ||||
| package view | ||||
| package views | ||||
| 
 | ||||
| import "embed" | ||||
| 
 | ||||
| Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										95
									
								
								pkg/views/src/components/AppShell.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								pkg/views/src/components/AppShell.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| import { | ||||
|   AppBar, | ||||
|   Avatar, | ||||
|   Box, | ||||
|   IconButton, | ||||
|   Slide, | ||||
|   Toolbar, | ||||
|   Typography, | ||||
|   useMediaQuery, | ||||
|   useScrollTrigger | ||||
| } from "@mui/material"; | ||||
| import { ReactElement, ReactNode, useEffect, useRef, useState } from "react"; | ||||
| import { SITE_NAME } from "@/consts"; | ||||
| import { Link } from "react-router-dom"; | ||||
| import NavigationMenu, { AppNavigationHeader, isMobileQuery } from "@/components/NavigationMenu.tsx"; | ||||
| import AccountCircleIcon from "@mui/icons-material/AccountCircleOutlined"; | ||||
| import { useUserinfo } from "@/stores/userinfo.tsx"; | ||||
|  | ||||
| function HideOnScroll(props: { window?: () => Window; children: ReactElement }) { | ||||
|   const { children, window } = props; | ||||
|   const trigger = useScrollTrigger({ | ||||
|     target: window ? window() : undefined | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <Slide appear={false} direction="down" in={!trigger}> | ||||
|       {children} | ||||
|     </Slide> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default function AppShell({ children }: { children: ReactNode }) { | ||||
|   let documentWindow: Window; | ||||
|  | ||||
|   const { userinfo } = useUserinfo(); | ||||
|  | ||||
|   const isMobile = useMediaQuery(isMobileQuery); | ||||
|   const [open, setOpen] = useState(false); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     documentWindow = window; | ||||
|   }, []); | ||||
|  | ||||
|   const container = useRef<HTMLDivElement>(null); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <HideOnScroll window={() => documentWindow}> | ||||
|         <AppBar position="fixed"> | ||||
|           <Toolbar sx={{ height: 64 }}> | ||||
|             <IconButton | ||||
|               size="large" | ||||
|               edge="start" | ||||
|               color="inherit" | ||||
|               aria-label="menu" | ||||
|               sx={{ ml: isMobile ? 0.5 : 0, mr: 2 }} | ||||
|             > | ||||
|               <img src="/favicon.svg" alt="Logo" width={32} height={32} /> | ||||
|             </IconButton> | ||||
|  | ||||
|             <Typography variant="h6" component="div" sx={{ flexGrow: 1, fontSize: "1.2rem" }}> | ||||
|               <Link to="/">{SITE_NAME}</Link> | ||||
|             </Typography> | ||||
|  | ||||
|             <IconButton | ||||
|               size="large" | ||||
|               edge="start" | ||||
|               color="inherit" | ||||
|               aria-label="menu" | ||||
|               onClick={() => setOpen(true)} | ||||
|               sx={{ mr: 1 }} | ||||
|             > | ||||
|               <Avatar | ||||
|                 sx={{ width: 32, height: 32, bgcolor: "transparent" }} | ||||
|                 ref={container} | ||||
|                 alt={userinfo?.displayName} | ||||
|                 src={userinfo?.profiles?.avatar} | ||||
|               > | ||||
|                 <AccountCircleIcon /> | ||||
|               </Avatar> | ||||
|             </IconButton> | ||||
|           </Toolbar> | ||||
|         </AppBar> | ||||
|       </HideOnScroll> | ||||
|  | ||||
|       <Box component="main"> | ||||
|         <AppNavigationHeader /> | ||||
|  | ||||
|         {children} | ||||
|       </Box> | ||||
|  | ||||
|       <NavigationMenu anchorEl={container.current} open={open} onClose={() => setOpen(false)} /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -1,27 +1,15 @@ | ||||
| import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; | ||||
| import ChevronRightIcon from "@mui/icons-material/ChevronRight"; | ||||
| import { | ||||
|   Box, | ||||
|   Collapse, | ||||
|   Divider, | ||||
|   Drawer, | ||||
|   IconButton, | ||||
|   List, | ||||
|   ListItemButton, | ||||
|   ListItemIcon, | ||||
|   ListItemText, | ||||
|   styled, | ||||
|   useMediaQuery | ||||
| } from "@mui/material"; | ||||
| import { Collapse, Divider, ListItemIcon, ListItemText, Menu, MenuItem, styled } from "@mui/material"; | ||||
| import { theme } from "@/theme"; | ||||
| import { Fragment, ReactNode, useState } from "react"; | ||||
| import HowToRegIcon from "@mui/icons-material/HowToReg"; | ||||
| import LoginIcon from "@mui/icons-material/Login"; | ||||
| import FaceIcon from "@mui/icons-material/Face"; | ||||
| import LogoutIcon from "@mui/icons-material/Logout"; | ||||
| import LogoutIcon from "@mui/icons-material/ExitToApp"; | ||||
| import ExpandLess from "@mui/icons-material/ExpandLess"; | ||||
| import ExpandMore from "@mui/icons-material/ExpandMore"; | ||||
| import { useUserinfo } from "@/stores/userinfo.tsx"; | ||||
| import { PopoverProps } from "@mui/material/Popover"; | ||||
| import { Link } from "react-router-dom"; | ||||
| 
 | ||||
| export interface NavigationItem { | ||||
|   icon?: ReactNode; | ||||
| @@ -51,32 +39,30 @@ export function AppNavigationSection({ items, depth }: { items: NavigationItem[] | ||||
|     } else if (item.children) { | ||||
|       return ( | ||||
|         <Fragment key={idx}> | ||||
|           <ListItemButton onClick={() => setOpen(!open)} sx={{ pl: 2 + (depth ?? 0) * 2 }}> | ||||
|           <MenuItem onClick={() => setOpen(!open)} sx={{ pl: 2 + (depth ?? 0) * 2, width: 180 }}> | ||||
|             <ListItemIcon>{item.icon}</ListItemIcon> | ||||
|             <ListItemText primary={item.title} /> | ||||
|             {open ? <ExpandLess /> : <ExpandMore />} | ||||
|           </ListItemButton> | ||||
|           </MenuItem> | ||||
|           <Collapse in={open} timeout="auto" unmountOnExit> | ||||
|             <List component="div" disablePadding> | ||||
|               <AppNavigationSection items={item.children} depth={(depth ?? 0) + 1} /> | ||||
|             </List> | ||||
|             <AppNavigationSection items={item.children} depth={(depth ?? 0) + 1} /> | ||||
|           </Collapse> | ||||
|         </Fragment> | ||||
|       ); | ||||
|     } else { | ||||
|       return ( | ||||
|         <a key={idx} href={item.link ?? "/"}> | ||||
|           <ListItemButton sx={{ pl: 2 + (depth ?? 0) * 2 }}> | ||||
|         <Link key={idx} to={item.link ?? "/"}> | ||||
|           <MenuItem sx={{ pl: 2 + (depth ?? 0) * 2, width: 180 }}> | ||||
|             <ListItemIcon>{item.icon}</ListItemIcon> | ||||
|             <ListItemText primary={item.title} /> | ||||
|           </ListItemButton> | ||||
|         </a> | ||||
|           </MenuItem> | ||||
|         </Link> | ||||
|       ); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onClose: () => void }) { | ||||
| export function AppNavigation() { | ||||
|   const { checkLoggedIn } = useUserinfo(); | ||||
| 
 | ||||
|   const nav: NavigationItem[] = [ | ||||
| @@ -94,61 +80,19 @@ export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onC | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <AppNavigationHeader> | ||||
|         {showClose && ( | ||||
|           <IconButton onClick={onClose}> | ||||
|             {theme.direction === "rtl" ? <ChevronLeftIcon /> : <ChevronRightIcon />} | ||||
|           </IconButton> | ||||
|         )} | ||||
|       </AppNavigationHeader> | ||||
|       <Divider /> | ||||
|       <List> | ||||
|         <AppNavigationSection items={nav} /> | ||||
|       </List> | ||||
|     </> | ||||
|   ); | ||||
|   return <AppNavigationSection items={nav} />; | ||||
| } | ||||
| 
 | ||||
| export const isMobileQuery = theme.breakpoints.down("md"); | ||||
| 
 | ||||
| export default function NavigationDrawer({ open, onClose }: { open: boolean; onClose: () => void }) { | ||||
|   const isMobile = useMediaQuery(isMobileQuery); | ||||
| 
 | ||||
|   return isMobile ? ( | ||||
|     <> | ||||
|       <Box sx={{ flexShrink: 0, width: DRAWER_WIDTH }} /> | ||||
|       <Drawer | ||||
|         keepMounted | ||||
|         anchor="right" | ||||
|         variant="temporary" | ||||
|         open={open} | ||||
|         onClose={onClose} | ||||
|         sx={{ | ||||
|           "& .MuiDrawer-paper": { | ||||
|             boxSizing: "border-box", | ||||
|             width: DRAWER_WIDTH | ||||
|           } | ||||
|         }} | ||||
|       > | ||||
|         <AppNavigation onClose={onClose} /> | ||||
|       </Drawer> | ||||
|     </> | ||||
|   ) : ( | ||||
|     <Drawer | ||||
|       variant="persistent" | ||||
|       anchor="right" | ||||
|       open={open} | ||||
|       sx={{ | ||||
|         width: DRAWER_WIDTH, | ||||
|         flexShrink: 0, | ||||
|         "& .MuiDrawer-paper": { | ||||
|           width: DRAWER_WIDTH | ||||
|         } | ||||
|       }} | ||||
|     > | ||||
|       <AppNavigation showClose onClose={onClose} /> | ||||
|     </Drawer> | ||||
| export default function NavigationMenu({ anchorEl, open, onClose }: { | ||||
|   anchorEl: PopoverProps["anchorEl"]; | ||||
|   open: boolean; | ||||
|   onClose: () => void | ||||
| }) { | ||||
|   return ( | ||||
|     <Menu anchorEl={anchorEl} open={open} onClose={onClose}> | ||||
|       <AppNavigation /> | ||||
|     </Menu> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user