From 89af1dad320c4be62926009f08b059e79f85702a Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Thu, 25 Jun 2026 07:57:01 +0800 Subject: [PATCH] refactor: split layout shell sections --- web/src/components/Layout.tsx | 218 ++----------- web/src/components/LayoutSections.tsx | 337 ++++++++++++++++++++ web/src/components/LayoutSidebarContent.tsx | 284 +++++++++++------ 3 files changed, 562 insertions(+), 277 deletions(-) create mode 100644 web/src/components/LayoutSections.tsx diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index 3ff9555..35ba82a 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -1,14 +1,13 @@ -import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom' -import { AnimatePresence, motion } from 'framer-motion' -import { Menu, MessageSquareText, Search, Sparkles } from 'lucide-react' -import clsx from 'clsx' -import { AppFooter } from './AppFooter' +import { useLocation, useNavigate } from 'react-router-dom' + import { useAuthStore } from '../stores/auth' import { usePlayProfileStore } from '../stores/playProfile' -import { LayoutSearchBox } from './LayoutSearchBox' -import { LayoutSidebarContent } from './LayoutSidebarContent' -import { LayoutThemeToggle } from './LayoutThemeToggle' -import { LayoutUserMenu } from './LayoutUserMenu' +import { + LayoutFrameFooter, + LayoutHeader, + LayoutSidebars, + LayoutWorkspace, +} from './LayoutSections' import { useLayoutSearch } from './useLayoutSearch' import { useLayoutPermissions } from './useLayoutPermissions' import { useLayoutProfiles } from './useLayoutProfiles' @@ -28,188 +27,35 @@ export function Layout() { locationSearch: location.search, navigate, }) - const { can, isAdmin } = useLayoutPermissions(user) - const { - isMobileDrawerOpen, - isRouteIn, - isSidebarOpen, - openGroups, - setIsMobileDrawerOpen, - setIsSidebarOpen, - toggleGroup, - } = useLayoutSidebar(location.pathname) - const { - activeProfile, - isProfileOpen, - profiles, - setIsProfileOpen, - switchProfile, - useDefaultProfile, - } = useLayoutProfiles({ activeProfileId, setActiveProfile, user }) + const permissions = useLayoutPermissions(user) + const sidebar = useLayoutSidebar(location.pathname) + const profile = useLayoutProfiles({ activeProfileId, setActiveProfile, user }) - const handleLogout = () => { - logout() - navigate('/login') - } - - const sidebarContent = ( - setIsSidebarOpen((current) => !current)} - onCloseMobileDrawer={() => setIsMobileDrawerOpen(false)} - onLogout={handleLogout} - /> - ) + const handleLogout = () => { logout(); navigate('/login') } + const closeProfileAndLogout = () => { profile.setIsProfileOpen(false); handleLogout() } return (
- - {/* 1. Desktop Persistent Sidebar */} - - - {/* 2. Mobile & Tablet Sidebar Drawer (Overlay) */} - - {isMobileDrawerOpen && ( -
- {/* Backdrop sheet */} - setIsMobileDrawerOpen(false)} - className="fixed inset-0 bg-black/15 backdrop-blur-sm" - /> - {/* Drawer body container */} - - {sidebarContent} - -
- )} -
- - {/* 3. Main Workspace Container */} +
- - {/* Top Header Bar */} -
- - {/* Header Left Area: Hamburger and Search */} -
- {/* Hamburger button shown on Mobile & Tablet */} - - - -
- - {/* Header Right Area: Actions, Notifications, Profile Dropdown */} -
- {/* Quick search button shown ONLY on Mobile screens */} - - - - - {/* Quick Discover shortcut button */} - {can('can_view_discover') && ( - - - 发现新片 - - )} - - {/* Notification channel settings */} - {isAdmin && ( - - - - )} - - {/* Horizontal divider lines */} - - - - - setIsProfileOpen((open) => !open)} - onClose={() => setIsProfileOpen(false)} - onUseDefaultProfile={useDefaultProfile} - onSwitchProfile={switchProfile} - onLogout={() => { - setIsProfileOpen(false) - logout() - navigate('/login') - }} - /> -
-
- - {/* ── Dynamic Content Workspace Scroll Area ── */} -
-
- - - - - -
-
- - {/* Absolute Footer Frame */} - + sidebar.setIsMobileDrawerOpen(true)} + user={user} + activeProfileId={activeProfileId} + profile={profile} + onLogout={closeProfileAndLogout} + /> + +
) diff --git a/web/src/components/LayoutSections.tsx b/web/src/components/LayoutSections.tsx new file mode 100644 index 0000000..261d096 --- /dev/null +++ b/web/src/components/LayoutSections.tsx @@ -0,0 +1,337 @@ +import { Link, Outlet } from 'react-router-dom' +import { AnimatePresence, motion } from 'framer-motion' +import { Menu, MessageSquareText, Search, Sparkles } from 'lucide-react' +import clsx from 'clsx' + +import type { PlayProfile, User } from '../types' +import { AppFooter } from './AppFooter' +import { LayoutSearchBox } from './LayoutSearchBox' +import { LayoutSidebarContent, type LayoutSidebarContentProps } from './LayoutSidebarContent' +import { LayoutThemeToggle } from './LayoutThemeToggle' +import { LayoutUserMenu } from './LayoutUserMenu' +import type { ThemeMode } from './useThemeMode' +import type { useLayoutSearch } from './useLayoutSearch' +import type { useLayoutProfiles } from './useLayoutProfiles' +import type { useLayoutSidebar } from './useLayoutSidebar' +import type { useThemeMode } from './useThemeMode' + +type LayoutSearchState = ReturnType +type LayoutProfileState = ReturnType +type LayoutSidebarState = ReturnType +type LayoutThemeState = ReturnType + +type LayoutPermissionState = { + can: (key: string) => boolean + isAdmin: boolean +} + +type LayoutSidebarProps = { + children: React.ReactNode + isSidebarOpen: boolean +} + +type LayoutMobileSidebarProps = { + children: React.ReactNode + isOpen: boolean + onClose: () => void +} + +type LayoutHeaderProps = { + search: LayoutSearchState + permissions: LayoutPermissionState + theme: LayoutThemeState + onOpenMobileDrawer: () => void + user: User | null | undefined + activeProfileId: string | null + profile: LayoutProfileState + onLogout: () => void +} + +type LayoutSidebarsProps = Omit< + LayoutSidebarContentProps, + 'isSidebarOpen' | 'isMobileDrawerOpen' | 'openGroups' | 'isRouteIn' | 'onToggleGroup' | 'onToggleSidebar' | 'onCloseMobileDrawer' +> & { + sidebar: LayoutSidebarState +} + +type LayoutWorkspaceProps = { + routeKey: string +} + +export function LayoutDesktopSidebar({ children, isSidebarOpen }: LayoutSidebarProps) { + return ( + + ) +} + +export function LayoutMobileSidebar({ children, isOpen, onClose }: LayoutMobileSidebarProps) { + return ( + + {isOpen && ( +
+ + + {children} + +
+ )} +
+ ) +} + +export function LayoutSidebars({ + sidebar, + isAdmin, + username, + can, + onLogout, +}: LayoutSidebarsProps) { + const content = ( + sidebar.setIsSidebarOpen((current) => !current)} + onCloseMobileDrawer={() => sidebar.setIsMobileDrawerOpen(false)} + onLogout={onLogout} + /> + ) + + return ( + <> + {content} + sidebar.setIsMobileDrawerOpen(false)} + > + {content} + + + ) +} + +export function LayoutHeader({ + search, + permissions, + theme, + onOpenMobileDrawer, + user, + activeProfileId, + profile, + onLogout, +}: LayoutHeaderProps) { + return ( +
+ + profile.setIsProfileOpen((open) => !open)} + onCloseProfile={() => profile.setIsProfileOpen(false)} + onUseDefaultProfile={profile.useDefaultProfile} + onSwitchProfile={profile.switchProfile} + onLogout={onLogout} + /> +
+ ) +} + +export function LayoutWorkspace({ routeKey }: LayoutWorkspaceProps) { + return ( +
+
+ + + + + +
+
+ ) +} + +export function LayoutFrameFooter() { + return ( + + ) +} + +export { LayoutSidebarContent } + +function LayoutHeaderSearch({ + search, + onOpenMobileDrawer, +}: { + search: LayoutSearchState + onOpenMobileDrawer: () => void +}) { + return ( +
+ + +
+ ) +} + +type LayoutHeaderActionsProps = { + permissions: LayoutPermissionState + themeMode: ThemeMode + onThemeChange: (mode: ThemeMode) => void + user: User | null | undefined + isProfileOpen: boolean + profiles: PlayProfile[] + activeProfileId: string | null + activeProfile: PlayProfile | null + onToggleProfile: () => void + onCloseProfile: () => void + onUseDefaultProfile: () => void + onSwitchProfile: (profile: PlayProfile) => void + onLogout: () => void +} + +function LayoutHeaderActions({ + permissions, + themeMode, + onThemeChange, + user, + isProfileOpen, + profiles, + activeProfileId, + activeProfile, + onToggleProfile, + onCloseProfile, + onUseDefaultProfile, + onSwitchProfile, + onLogout, +}: LayoutHeaderActionsProps) { + return ( +
+ + + + +
+ ) +} + +function LayoutQuickActions({ permissions }: { permissions: LayoutPermissionState }) { + return ( + <> + + + + {permissions.can('can_view_discover') && ( + + + 发现新片 + + )} + {permissions.isAdmin && ( + + + + )} + + ) +} + +function LayoutProfileMenu({ + user, + isProfileOpen, + profiles, + activeProfileId, + activeProfile, + onToggleProfile, + onCloseProfile, + onUseDefaultProfile, + onSwitchProfile, + onLogout, +}: Omit) { + return ( + + ) +} diff --git a/web/src/components/LayoutSidebarContent.tsx b/web/src/components/LayoutSidebarContent.tsx index 33df06a..d127c0d 100644 --- a/web/src/components/LayoutSidebarContent.tsx +++ b/web/src/components/LayoutSidebarContent.tsx @@ -2,10 +2,10 @@ import { Link } from 'react-router-dom' import { motion } from 'framer-motion' import { LogOut, Menu, X } from 'lucide-react' import clsx from 'clsx' -import { LAYOUT_NAV_GROUPS, NAV_GROUP_PATHS, type LayoutNavItem } from './layoutNavigation' +import { LAYOUT_NAV_GROUPS, NAV_GROUP_PATHS, type LayoutNavGroup, type LayoutNavItem } from './layoutNavigation' import { SidebarGroup, SidebarLink } from './LayoutSidebarNav' -type LayoutSidebarContentProps = { +export type LayoutSidebarContentProps = { isSidebarOpen: boolean isMobileDrawerOpen: boolean openGroups: Record @@ -19,6 +19,11 @@ type LayoutSidebarContentProps = { onLogout: () => void } +type VisibleLayoutNavGroup = { + group: LayoutNavGroup + items: LayoutNavItem[] +} + export function LayoutSidebarContent({ isSidebarOpen, isMobileDrawerOpen, @@ -33,98 +38,195 @@ export function LayoutSidebarContent({ onLogout, }: LayoutSidebarContentProps) { const sidebarExpanded = isSidebarOpen || isMobileDrawerOpen - const isItemVisible = (item: LayoutNavItem) => - (!item.adminOnly || isAdmin) && (!item.permission || can(item.permission)) - const visibleGroups = LAYOUT_NAV_GROUPS - .filter((group) => !group.adminOnly || isAdmin) - .map((group) => ({ - group, - items: group.items.filter(isItemVisible), - })) - .filter(({ items }) => items.length > 0) + const visibleGroups = visibleSidebarGroups({ isAdmin, can }) return (
-
- - MediaStationGo - {sidebarExpanded && ( - - MediaStationGo - - )} - - - - - -
- - - -
- -
+ + +
) } + +function visibleSidebarGroups({ + isAdmin, + can, +}: Pick): VisibleLayoutNavGroup[] { + const isItemVisible = (item: LayoutNavItem) => + (!item.adminOnly || isAdmin) && (!item.permission || can(item.permission)) + return LAYOUT_NAV_GROUPS + .filter((group) => !group.adminOnly || isAdmin) + .map((group) => ({ group, items: group.items.filter(isItemVisible) })) + .filter(({ items }) => items.length > 0) +} + +function LayoutSidebarHeader({ + sidebarExpanded, + onToggleSidebar, + onCloseMobileDrawer, +}: { + sidebarExpanded: boolean + onToggleSidebar: () => void + onCloseMobileDrawer: () => void +}) { + return ( +
+ + MediaStationGo + {sidebarExpanded && ( + + MediaStationGo + + )} + + + + + + + +
+ ) +} + +function LayoutSidebarNav({ + groups, + sidebarExpanded, + openGroups, + isRouteIn, + onToggleGroup, +}: { + groups: VisibleLayoutNavGroup[] + sidebarExpanded: boolean + openGroups: Record + isRouteIn: (paths: string[]) => boolean + onToggleGroup: (id: string) => void +}) { + return ( + + ) +} + +function LayoutSidebarNavGroup({ + group, + items, + sidebarExpanded, + open, + active, + onToggleGroup, +}: { + group: LayoutNavGroup + items: LayoutNavItem[] + sidebarExpanded: boolean + open: boolean + active: boolean + onToggleGroup: (id: string) => void +}) { + const GroupIcon = group.icon + return ( + } + label={group.label} + collapsed={!sidebarExpanded} + open={open} + active={active} + onToggle={onToggleGroup} + > + {items.map((item) => { + const ItemIcon = item.icon + return ( + } + label={item.label} + end={item.end} + child + /> + ) + })} + + ) +} + +function LayoutSidebarLogout({ + sidebarExpanded, + username, + onLogout, +}: { + sidebarExpanded: boolean + username?: string + onLogout: () => void +}) { + return ( +
+ +
+ ) +} + +function SidebarIconButton({ + children, + className, + onClick, +}: { + children: React.ReactNode + className: string + onClick: () => void +}) { + return ( + + ) +}