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

ModeValueDescriptionUse Case
Side NavigationsideVertical sidebar menu + top headerDefault layout, suitable for most admin panels
Top NavigationtopHorizontal top menuFewer menu items, wider content area
Mixed NavigationmixTop-level menu in header + secondary menu in sidebarDeep menu hierarchy needing grouping
Columns NavigationcolumnsLeft icon bar + expandable side menuMany top-level groups, compact sidebar

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

FieldTypeDescription
menusIMenus[]Full menu tree fetched from API
parentKeystring | undefinedCurrently selected top-level menu key
selectKeystring[]Selected menu key path (breadcrumb chain)
collapsedbooleanSidebar collapsed state
setCollapsed(collapsed: boolean) => voidSet collapsed state
setParentKey(key: string) => voidSet parent menu
setSelectKey(keys: string[]) => voidSet selected path

Initialization Flow

  1. Calls the menu() API on mount to fetch menu data
  2. Matches the current URL path to menus, auto-restoring expansion and selection
  3. 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

typeDescriptionClick Behavior
menuMenu groupExpand/collapse children, or switch parent (mix mode)
routeRoute linkNavigate to the route; external links open in new window
rulePermission ruleNot rendered, for permission control only

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}
/>

Header (HeaderRender)

The header has separate desktop and mobile renderings, containing these common elements:

Desktop

ElementDescription
Logo + TitleBrand identity, supports custom image and text
Collapse ButtonShown in side/mix layout, toggles sidebar collapse
BreadcrumbShown in side/columns layout, path navigation
Top MenuRenders as horizontal menu (top) or top-level menu (mix)
Right ActionsSearch, fullscreen, language switch, theme settings, user info

Mobile

Mobile header is simplified, showing only Logo + Title + right actions. The menu is accessed via a floating button that opens a drawer.

Header Right Actions (HeaderRightRender)

<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

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.

Mobile Menu (MobileDrawerMenu)

  • 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

ThemeDescription
lightLight theme (default), white background
darkDark theme, dark background

Theme Algorithms

AlgorithmDescription
defaultAlgorithmAnt Design default algorithm
darkAlgorithmAnt Design dark algorithm
defaultCompactAlgorithmDefault + compact algorithm
darkCompactAlgorithmDark + compact algorithm

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:

FunctionDescription
buildBreadcrumbMap(menus)Build breadcrumb mapping, returns { [key]: BreadcrumbItem[] }
findMenuByPath(menus, path)Find menu item by route path
findMenuByKey(menus, key)Find menu item by key (recursive)
getMenuParentKeys(menus, path)Get all parent menu keys from root to a given path

Complete Examples

Initialize Site Information

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>;
}

Programmatic Menu Control

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>
  );
}