Frontend Layout
XinAdmin provides a flexible frontend layout system with four layout modes, comprehensive theme customization, mobile adaptation, and menu management.
Layout Architecture
The layout system consists of the following core modules:
layout/
├── index.tsx # Main layout component
├── LayoutContext.tsx # Layout context (menu state management)
├── HeaderRender.tsx # Top header
├── HeaderRightRender.tsx # Header right-side actions
├── MenuRender.tsx # Menu rendering
├── ColumnsMenu.tsx # Columns layout menu
├── BreadcrumbRender.tsx # Breadcrumb navigation
├── FooterRender.tsx # Page footer
├── MobileDrawerMenu.tsx # Mobile drawer menu
├── SettingDrawer.tsx # Theme settings drawer
├── theme.ts # Theme config constants
├── algorithm.ts # Theme algorithms
├── typing.ts # Type definitions
└── utils.ts # Utility functions
The LayoutRender component wraps everything with LayoutProvider, which initializes menu data and automatically restores menu expansion state on URL changes.
// Entry: LayoutProvider wraps the entire layout
const LayoutRender = () => (
<LayoutProvider>
<LayoutContent />
</LayoutProvider>
);
Four Layout Modes
Layout Comparison
side
- Header: Logo + title + collapse button + breadcrumb + right actions
- Sidebar: Full tree menu
- Best for: Standard admin panels
top
- Header: Logo + title + horizontal menu + right actions
- Sidebar: None
- Best for: Simple apps with fewer than 7 menu items
mix
- Header: Logo + title + collapse button + horizontal top-level menu + right actions
- Sidebar: Secondary menu of the selected top-level item
- Best for: Large apps with deep menu hierarchy
columns
- Header: Same as side
- Sidebar: Left icon bar (top-level) + right expanded panel (secondary)
- Best for: Many top-level groups requiring quick switching
Layout switching is controlled via the global layout state. Users can switch visually in the theme settings drawer.
// Switch layout via store
const setLayout = useGlobalStore(state => state.setLayout);
setLayout('columns');
Layout Context
LayoutContext is the core state management center for the layout system, responsible for menu data and interaction state.
State Fields
Initialization Flow
- Calls the
menu() API on mount to fetch menu data
- Matches the current URL path to menus, auto-restoring expansion and selection
- Uses
getMenuParentKeys() to build the key chain from root to the current item
import { useLayoutContext } from '@/layout/LayoutContext';
function MyComponent() {
const { menus, selectKey, setCollapsed, collapsed } = useLayoutContext();
// ...
}
Menu data is fetched via the /api/system/sys_rule/menu API, with the following structure:
interface IMenus {
id?: number;
pid?: number; // Parent menu ID, 0 for top-level
type?: 'menu' | 'route' | 'rule'; // Menu group / route link / permission rule
key?: string; // Unique identifier
name?: string; // Display name (used when no i18n key)
path?: string; // Route path
icon?: string; // Icon (iconfont name)
local?: string; // i18n key
hidden?: number; // Hidden flag (0: hidden, 1: visible)
status?: number; // Enabled flag
link?: number; // External link (1: open in new window)
children?: IMenus[]; // Child menus
}
Type Descriptions
MenuRender converts menu data to Ant Design Menu-compatible format with the following logic:
- Filters out menu items where
hidden is 0 or type is not route/menu
- Recursively builds the menu tree structure
- Auto-switches between horizontal/inline mode based on layout type
- In mix/columns mode, only renders children of the selected top-level menu
- Supports i18n translation via the
local field
// Core menu rendering logic
<Menu
mode={layout === 'top' ? 'horizontal' : 'inline'}
items={menuItems}
onSelect={(info) => onMenuChange(info.keyPath)}
selectedKeys={selectKey}
/>
The header has separate desktop and mobile renderings, containing these common elements:
Desktop
Mobile
Mobile header is simplified, showing only Logo + Title + right actions. The menu is accessed via a floating button that opens a drawer.
<Space>
<Button icon={<HomeOutlined/>} /> {/* Official website */}
<Button icon={<GithubOutlined/>} /> {/* GitHub */}
<Button icon={<SearchOutlined/>} /> {/* Menu search */}
<Button>Fullscreen toggle</Button> {/* Browser fullscreen */}
<LanguageSwitcher /> {/* zh/en switch */}
<Button icon={<SettingOutlined/>} /> {/* Theme settings */}
<Dropdown> {/* User info */}
<Avatar /> + Nickname
{/* Dropdown: Profile / Logout */}
</Dropdown>
</Space>
Search Modal
Clicking the search button opens a Modal with keyboard shortcut hints:
Enter — Confirm selection
↑ ↓ — Switch options
Esc — Close search
Breadcrumb (BreadcrumbRender)
Breadcrumbs use the buildBreadcrumbMap() utility to pre-build path mappings for all menus, then match the current URL for display.
// Build breadcrumb mapping
buildBreadcrumbMap(menus);
// Output format: { [menuKey]: [{ title, icon, href, local }, ...] }
// Rendered result:
// Home > System > User List
Breadcrumbs update automatically on menu changes. A default home breadcrumb is shown when no menu matches.
Supports both fixed and relative modes, controlled by themeConfig.fixedFooter:
// Fixed: footer sticks to bottom, content area auto-pads
// Relative: footer follows content flow
<Footer style={{ borderTop: '1px solid ' + themeConfig.colorBorder }}>
Xin Admin ©{year} Created by xiaoliu
</Footer>
Mobile Adaptation
Mobile devices are detected via the useMobile() hook and use a separate menu display approach.
- Floating circular button at bottom-left triggers left drawer
- Drawer renders the full menu (
MenuRender)
- Drawer footer includes quick actions: Home, GitHub, language switch, theme settings
- Sidebar is completely hidden on mobile, header is simplified
// Mobile detection
const isMobile = useMobile();
// Conditional rendering
{isMobile && <MobileDrawerMenu />}
{layout !== "top" && !isMobile && <Sider>...</Sider>}
Theme System
Theme Configuration (ThemeProps)
Theme configuration is managed via Zustand store with localStorage persistence:
interface ThemeProps {
// Color system
themeScheme: 'light' | 'dark' | 'pink' | 'green';
colorPrimary: string; // Brand color (default #1677ff)
colorError: string; // Error color
colorSuccess: string; // Success color
colorWarning: string; // Warning color
colorText: string; // Base text color
colorBg: string; // Base background color
background: string; // Global background
// Area colors
bodyBg: string; // Content area background
footerBg: string; // Footer background
headerBg: string; // Header background
headerColor: string; // Header text color
siderBg: string; // Sidebar background
siderColor: string; // Sidebar text color
colorBorder: string; // Border/divider color
// Dimension config
borderRadius: number; // Border radius (default 10)
controlHeight: number; // Control height (default 32)
headerPadding: number; // Header padding (default 20)
headerHeight: number; // Header height (default 56)
siderWeight: number; // Sidebar width (default 226)
bodyPadding: number; // Content area padding (default 20)
fixedFooter: boolean; // Fixed footer (default false)
// Algorithm
algorithm: algorithmType; // Theme algorithm
}
Preset Themes
Theme Algorithms
Custom Theme
const setThemeConfig = useGlobalStore(state => state.setThemeConfig);
// Modify a single config
setThemeConfig({
...currentConfig,
colorPrimary: '#52c41a',
borderRadius: 4,
});
// Use theme tokens in components (via Ant Design useToken)
const { token } = theme.useToken();
<div style={{ background: token.colorPrimary }} />
Theme Settings Drawer (SettingDrawer)
A visual theme configuration panel opened via the gear icon in the top-right corner, divided into three sections:
1. Layout Style
Four layout modes displayed as thumbnails, click to switch instantly. The currently selected mode shows a brand-color border.
2. Preset Theme
- One-click switch between light and dark themes
- Theme algorithm selector
3. Theme Colors
Independently configure 13 theme colors via ColorPicker:
- Brand color, text color, background color
- Success, warning, error colors
- Content area background, footer background
- Header background/text color
- Sidebar background/text color
- Border/divider color
4. Style Configuration
Adjust layout dimension parameters: fixed footer, border radius, control height, header height, sidebar width, content area padding, etc.
All configuration changes take effect immediately with 400ms debounce and are automatically persisted to localStorage.
Global State Management
Layout-related state is managed via a Zustand store:
interface GlobalStore {
// Site info
logo: string; // Logo image URL
title: string; // Site title
subtitle: string; // Subtitle
describe: string; // Description
// Layout
layout: LayoutType; // Current layout mode
themeConfig: ThemeProps; // Current theme config
themeDrawer: boolean; // Theme settings drawer visibility
// Actions
setLayout: (layout: LayoutType) => void;
setThemeConfig: (themeConfig: ThemeProps) => void;
setThemeDrawer: (open: boolean) => void;
initWebInfo: () => Promise<void>; // Initialize site info
}
State is persisted to localStorage via the persist middleware and debuggable via the devtools middleware in Redux DevTools.
Utility Functions
The layout system provides the following utility functions in layout/utils.ts:
Complete Examples
import { useEffect } from 'react';
import useGlobalStore from '@/stores/global';
function AppInit() {
const initWebInfo = useGlobalStore(state => state.initWebInfo);
useEffect(() => {
initWebInfo(); // Fetch logo/title config from API
}, []);
return null;
}
Dynamically Change Theme
import useGlobalStore from '@/stores/global';
function ThemeToggle() {
const themeConfig = useGlobalStore(state => state.themeConfig);
const setThemeConfig = useGlobalStore(state => state.setThemeConfig);
const toggleDark = () => {
setThemeConfig({
...themeConfig,
themeScheme: 'dark',
bodyBg: '#000',
headerBg: '#141414',
siderBg: '#141414',
colorText: '#fff',
colorBg: '#000',
colorBorder: '#282828',
algorithm: 'darkAlgorithm',
});
};
return <Button onClick={toggleDark}>Toggle Dark</Button>;
}
import { useLayoutContext } from '@/layout/LayoutContext';
function CustomMenuControl() {
const { collapsed, setCollapsed, menus, selectKey, setSelectKey } = useLayoutContext();
return (
<div>
<Button onClick={() => setCollapsed(!collapsed)}>Toggle Menu</Button>
<span>Selected: {selectKey.join(' > ')}</span>
<span>Menu count: {menus.length}</span>
</div>
);
}