♻️ 按照 Material Design + Reactjs 重构 #1
| @@ -1,7 +1,7 @@ | |||||||
| package server | package server | ||||||
|  |  | ||||||
| import ( | 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" | ||||||
| 	"github.com/gofiber/fiber/v2/middleware/cache" | 	"github.com/gofiber/fiber/v2/middleware/cache" | ||||||
| 	"github.com/gofiber/fiber/v2/middleware/cors" | 	"github.com/gofiber/fiber/v2/middleware/cors" | ||||||
| @@ -92,7 +92,7 @@ func NewServer() { | |||||||
| 		Expiration:   24 * time.Hour, | 		Expiration:   24 * time.Hour, | ||||||
| 		CacheControl: true, | 		CacheControl: true, | ||||||
| 	}), filesystem.New(filesystem.Config{ | 	}), filesystem.New(filesystem.Config{ | ||||||
| 		Root:         http.FS(view.FS), | 		Root:         http.FS(views.FS), | ||||||
| 		PathPrefix:   "dist", | 		PathPrefix:   "dist", | ||||||
| 		Index:        "index.html", | 		Index:        "index.html", | ||||||
| 		NotFoundFile: "dist/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" | 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 { Collapse, Divider, ListItemIcon, ListItemText, Menu, MenuItem, styled } from "@mui/material"; | ||||||
| import ChevronRightIcon from "@mui/icons-material/ChevronRight"; |  | ||||||
| import { |  | ||||||
|   Box, |  | ||||||
|   Collapse, |  | ||||||
|   Divider, |  | ||||||
|   Drawer, |  | ||||||
|   IconButton, |  | ||||||
|   List, |  | ||||||
|   ListItemButton, |  | ||||||
|   ListItemIcon, |  | ||||||
|   ListItemText, |  | ||||||
|   styled, |  | ||||||
|   useMediaQuery |  | ||||||
| } from "@mui/material"; |  | ||||||
| import { theme } from "@/theme"; | import { theme } from "@/theme"; | ||||||
| import { Fragment, ReactNode, useState } from "react"; | import { Fragment, ReactNode, useState } from "react"; | ||||||
| import HowToRegIcon from "@mui/icons-material/HowToReg"; | import HowToRegIcon from "@mui/icons-material/HowToReg"; | ||||||
| import LoginIcon from "@mui/icons-material/Login"; | import LoginIcon from "@mui/icons-material/Login"; | ||||||
| import FaceIcon from "@mui/icons-material/Face"; | 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 ExpandLess from "@mui/icons-material/ExpandLess"; | ||||||
| import ExpandMore from "@mui/icons-material/ExpandMore"; | import ExpandMore from "@mui/icons-material/ExpandMore"; | ||||||
| import { useUserinfo } from "@/stores/userinfo.tsx"; | import { useUserinfo } from "@/stores/userinfo.tsx"; | ||||||
|  | import { PopoverProps } from "@mui/material/Popover"; | ||||||
|  | import { Link } from "react-router-dom"; | ||||||
| 
 | 
 | ||||||
| export interface NavigationItem { | export interface NavigationItem { | ||||||
|   icon?: ReactNode; |   icon?: ReactNode; | ||||||
| @@ -51,32 +39,30 @@ export function AppNavigationSection({ items, depth }: { items: NavigationItem[] | |||||||
|     } else if (item.children) { |     } else if (item.children) { | ||||||
|       return ( |       return ( | ||||||
|         <Fragment key={idx}> |         <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> |             <ListItemIcon>{item.icon}</ListItemIcon> | ||||||
|             <ListItemText primary={item.title} /> |             <ListItemText primary={item.title} /> | ||||||
|             {open ? <ExpandLess /> : <ExpandMore />} |             {open ? <ExpandLess /> : <ExpandMore />} | ||||||
|           </ListItemButton> |           </MenuItem> | ||||||
|           <Collapse in={open} timeout="auto" unmountOnExit> |           <Collapse in={open} timeout="auto" unmountOnExit> | ||||||
|             <List component="div" disablePadding> |             <AppNavigationSection items={item.children} depth={(depth ?? 0) + 1} /> | ||||||
|               <AppNavigationSection items={item.children} depth={(depth ?? 0) + 1} /> |  | ||||||
|             </List> |  | ||||||
|           </Collapse> |           </Collapse> | ||||||
|         </Fragment> |         </Fragment> | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       return ( |       return ( | ||||||
|         <a key={idx} href={item.link ?? "/"}> |         <Link key={idx} to={item.link ?? "/"}> | ||||||
|           <ListItemButton sx={{ pl: 2 + (depth ?? 0) * 2 }}> |           <MenuItem sx={{ pl: 2 + (depth ?? 0) * 2, width: 180 }}> | ||||||
|             <ListItemIcon>{item.icon}</ListItemIcon> |             <ListItemIcon>{item.icon}</ListItemIcon> | ||||||
|             <ListItemText primary={item.title} /> |             <ListItemText primary={item.title} /> | ||||||
|           </ListItemButton> |           </MenuItem> | ||||||
|         </a> |         </Link> | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onClose: () => void }) { | export function AppNavigation() { | ||||||
|   const { checkLoggedIn } = useUserinfo(); |   const { checkLoggedIn } = useUserinfo(); | ||||||
| 
 | 
 | ||||||
|   const nav: NavigationItem[] = [ |   const nav: NavigationItem[] = [ | ||||||
| @@ -94,61 +80,19 @@ export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onC | |||||||
|     ) |     ) | ||||||
|   ]; |   ]; | ||||||
| 
 | 
 | ||||||
|   return ( |   return <AppNavigationSection items={nav} />; | ||||||
|     <> |  | ||||||
|       <AppNavigationHeader> |  | ||||||
|         {showClose && ( |  | ||||||
|           <IconButton onClick={onClose}> |  | ||||||
|             {theme.direction === "rtl" ? <ChevronLeftIcon /> : <ChevronRightIcon />} |  | ||||||
|           </IconButton> |  | ||||||
|         )} |  | ||||||
|       </AppNavigationHeader> |  | ||||||
|       <Divider /> |  | ||||||
|       <List> |  | ||||||
|         <AppNavigationSection items={nav} /> |  | ||||||
|       </List> |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const isMobileQuery = theme.breakpoints.down("md"); | export const isMobileQuery = theme.breakpoints.down("md"); | ||||||
| 
 | 
 | ||||||
| export default function NavigationDrawer({ open, onClose }: { open: boolean; onClose: () => void }) { | export default function NavigationMenu({ anchorEl, open, onClose }: { | ||||||
|   const isMobile = useMediaQuery(isMobileQuery); |   anchorEl: PopoverProps["anchorEl"]; | ||||||
| 
 |   open: boolean; | ||||||
|   return isMobile ? ( |   onClose: () => void | ||||||
|     <> | }) { | ||||||
|       <Box sx={{ flexShrink: 0, width: DRAWER_WIDTH }} /> |   return ( | ||||||
|       <Drawer |     <Menu anchorEl={anchorEl} open={open} onClose={onClose}> | ||||||
|         keepMounted |       <AppNavigation /> | ||||||
|         anchor="right" |     </Menu> | ||||||
|         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> |  | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user