whistle.mock-plugins
Version:
Whistle 插件,用于快速创建 API 模拟数据
408 lines (378 loc) • 13.5 kB
JavaScript
import React, { useState, useMemo, useEffect } from 'react';
import { Layout, Menu, Button, Breadcrumb, Space, ConfigProvider, theme } from 'antd';
import {
AppstoreOutlined,
ApiOutlined,
SettingOutlined,
FileTextOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
ExperimentOutlined,
BarChartOutlined,
ThunderboltOutlined
} from '@ant-design/icons';
import { Link, useLocation } from 'react-router-dom';
import VersionModal from './VersionModal';
import KeyboardShortcutsHelp from './KeyboardShortcutsHelp';
import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts';
import '../styles/app-layout.css';
const { Header, Content, Sider } = Layout;
const THEMES = ['light', 'cyber', 'earth'];
const AppLayout = ({ children }) => {
const location = useLocation();
const currentPath = location.pathname;
const [collapsed, setCollapsed] = useState(false);
const [versionModalVisible, setVersionModalVisible] = useState(false);
const [shortcutsHelpVisible, setShortcutsHelpVisible] = useState(false);
// 注册全局键盘快捷键
useKeyboardShortcuts();
// 监听快捷键帮助事件
useEffect(() => {
const handleHelp = () => setShortcutsHelpVisible(true);
window.addEventListener('keyboard-shortcuts-help', handleHelp);
return () => window.removeEventListener('keyboard-shortcuts-help', handleHelp);
}, []);
// 主题切换(兼容旧版 cyberTheme 存储)
const [currentTheme, setCurrentTheme] = useState(() => {
const saved = localStorage.getItem('theme');
if (saved && THEMES.includes(saved)) return saved;
// 兼容旧版 boolean 存储
const oldCyber = localStorage.getItem('cyberTheme');
return oldCyber === 'false' ? 'light' : oldCyber === 'true' ? 'cyber' : 'light';
});
useEffect(() => {
document.body.classList.remove('cyber-theme', 'earth-theme');
if (currentTheme !== 'light') {
document.body.classList.add(`${currentTheme}-theme`);
}
}, [currentTheme]);
useEffect(() => {
const handleStorage = () => {
const saved = localStorage.getItem('theme');
if (saved && THEMES.includes(saved)) {
setCurrentTheme(saved);
} else {
const oldCyber = localStorage.getItem('cyberTheme');
setCurrentTheme(oldCyber === 'false' ? 'light' : 'cyber');
}
};
window.addEventListener('storage', handleStorage);
return () => window.removeEventListener('storage', handleStorage);
}, []);
const cyberTheme = useMemo(() => ({
token: {
colorPrimary: '#00f0ff',
colorPrimaryHover: '#33f3ff',
colorPrimaryActive: '#00c2cc',
colorSuccess: '#00ff41',
colorWarning: '#f0e800',
colorError: '#ff4d4f',
colorInfo: '#00f0ff',
colorText: '#e0e0e0',
colorTextSecondary: '#a0a0a0',
colorTextTertiary: '#6b7280',
colorBorder: '#1a1a2e',
colorBorderSecondary: '#1a1a2e',
colorBgBase: '#050508',
colorBgContainer: '#0f0f1a',
colorBgElevated: '#141420',
colorBgLayout: '#0a0a12',
colorBgSpotlight: '#141420',
borderRadius: 6,
borderRadiusSM: 4,
borderRadiusLG: 8,
wireframe: false,
fontFamily: "'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
fontFamilyCode: "'Share Tech Mono', 'SFMono-Regular', Consolas, monospace",
},
algorithm: theme.darkAlgorithm,
}), []);
const earthTheme = useMemo(() => ({
token: {
colorPrimary: '#b87333',
colorPrimaryHover: '#c9894d',
colorPrimaryActive: '#9a5f2a',
colorSuccess: '#6b8e23',
colorWarning: '#d4a017',
colorError: '#c0392b',
colorInfo: '#8d6e63',
colorText: '#3d3630',
colorTextSecondary: '#6d6459',
colorTextTertiary: '#9e9488',
colorBorder: '#e0d8cf',
colorBorderSecondary: '#d5cdc2',
colorBgBase: '#faf6f0',
colorBgContainer: '#fdfcfa',
colorBgElevated: '#f5f0e8',
colorBgLayout: '#f0ebe3',
colorBgSpotlight: '#f5f0e8',
borderRadius: 6,
borderRadiusSM: 4,
borderRadiusLG: 8,
wireframe: false,
fontFamily: "'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
fontFamilyCode: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
},
algorithm: theme.defaultAlgorithm,
}), []);
const lightTheme = useMemo(() => ({
token: {
borderRadius: 6,
borderRadiusSM: 4,
borderRadiusLG: 8,
},
algorithm: theme.defaultAlgorithm,
}), []);
const activeTheme = useMemo(() => {
if (currentTheme === 'cyber') return cyberTheme;
if (currentTheme === 'earth') return earthTheme;
return lightTheme;
}, [currentTheme, cyberTheme, earthTheme, lightTheme]);
// 主题颜色映射(用于 inline style)
const t = useMemo(() => {
switch (currentTheme) {
case 'cyber':
return {
headerBg: '#0f0f1a',
headerBorder: '#1a1a2e',
logoIcon: '#00ff41',
logoGradient: 'linear-gradient(135deg, #00f0ff, #00ff41)',
versionColor: '#6b7280',
siderBg: '#0f0f1a',
wrapperBg: '#0a0a12',
breadcrumbLink: '#a0a0a0',
breadcrumbCurrent: '#e0e0e0',
titleColor: '#e0e0e0',
titleFont: "'Orbitron', sans-serif",
titleLetterSpacing: '0.5px',
contentBg: '#0f0f1a',
contentBorder: '#1a1a2e',
};
case 'earth':
return {
headerBg: '#fdfcfa',
headerBorder: '#e0d8cf',
logoIcon: '#b87333',
logoGradient: 'linear-gradient(135deg, #b87333, #d4a017)',
versionColor: '#6d6459',
siderBg: '#f5f0e8',
wrapperBg: '#f0ebe3',
breadcrumbLink: '#6d6459',
breadcrumbCurrent: '#3d3630',
titleColor: '#3d3630',
titleFont: undefined,
titleLetterSpacing: undefined,
contentBg: '#fdfcfa',
contentBorder: '#e0d8cf',
};
default: // light
return {
headerBg: '#ffffff',
headerBorder: '#f0f0f0',
logoIcon: '#52c41a',
logoGradient: 'linear-gradient(135deg, #1890ff, #52c41a)',
versionColor: '#000000d9',
siderBg: '#ffffff',
wrapperBg: '#f5f5f5',
breadcrumbLink: '#000000d9',
breadcrumbCurrent: '#000000d9',
titleColor: '#000000d9',
titleFont: undefined,
titleLetterSpacing: undefined,
contentBg: '#ffffff',
contentBorder: '#f0f0f0',
};
}
}, [currentTheme]);
// 版本弹窗控制方法
const showVersionModal = () => setVersionModalVisible(true);
const hideVersionModal = () => setVersionModalVisible(false);
// 根据当前路径获取选中的菜单项和面包屑
const getSelectedKeys = () => {
if (currentPath === '/' || currentPath === '#/') return ['1'];
if (currentPath.startsWith('/interface') || currentPath.includes('/interface')) return ['2'];
if (currentPath === '/stats' || currentPath.includes('/stats')) return ['4'];
if (currentPath === '/sniffer' || currentPath.includes('/sniffer')) return ['5'];
if (currentPath === '/settings' || currentPath.includes('/settings')) return ['3'];
return [];
};
// 根据当前路径获取面包屑
const getBreadcrumbs = () => {
const breadcrumbs = [
{ path: '/', title: '首页' }
];
if (currentPath.startsWith('/interface')) {
breadcrumbs.push({ path: '/interface', title: '接口管理' });
// 如果有featureId参数,添加特定功能模块名称
if (currentPath.includes('/interface/') && currentPath !== '/interface/') {
breadcrumbs.push({ path: currentPath, title: '功能接口详情' });
}
} else if (currentPath === '/stats') {
breadcrumbs.push({ path: '/stats', title: '请求统计' });
} else if (currentPath === '/sniffer') {
breadcrumbs.push({ path: '/sniffer', title: '接口嗅探' });
} else if (currentPath === '/settings') {
breadcrumbs.push({ path: '/settings', title: '系统设置' });
}
return breadcrumbs;
};
// 获取当前页面标题
const getPageTitle = () => {
if (currentPath === '/' || currentPath === '#/') return '功能模块管理';
if (currentPath.startsWith('/interface')) return '接口配置管理';
if (currentPath === '/stats') return '请求统计';
if (currentPath === '/sniffer') return '接口嗅探';
if (currentPath === '/settings') return '系统设置';
return 'Whistle Mock Plugin';
};
// 菜单配置
const menuItems = [
{
key: '1',
icon: <AppstoreOutlined />,
label: <Link to="/">功能模块</Link>,
},
{
key: '2',
icon: <ApiOutlined />,
label: <Link to="/interface">接口管理</Link>,
},
{
key: '4',
icon: <BarChartOutlined />,
label: <Link to="/stats">请求统计</Link>,
},
{
key: '5',
icon: <ThunderboltOutlined />,
label: <Link to="/sniffer">接口嗅探</Link>,
},
{
key: '3',
icon: <SettingOutlined />,
label: <Link to="/settings">系统设置</Link>,
}
];
const breadcrumbs = getBreadcrumbs();
return (
<ConfigProvider theme={activeTheme}>
<Layout className="app-layout">
{/* 顶部导航栏 */}
<Header className="app-header" style={{ background: t.headerBg, borderBottom: `1px solid ${t.headerBorder}` }}>
<div className="header-left">
<div className="logo-container">
<div className="logo-icon-group">
<ExperimentOutlined className="logo-icon logo-icon-secondary" style={{ color: t.logoIcon }} />
</div>
<div
className="logo-text"
style={{
background: t.logoGradient,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
MockMaster
</div>
</div>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
className="collapse-button"
/>
</div>
<div className="header-right">
<Space>
<Button
type="text"
size="small"
className="version-button"
onClick={showVersionModal}
>
<span style={{ color: t.versionColor, fontFamily: "'Share Tech Mono', monospace" }}>
v{__APP_VERSION__}
</span>
</Button>
</Space>
</div>
</Header>
<Layout>
{/* 侧边导航 */}
<Sider
width={220}
className="app-sider"
collapsed={collapsed}
collapsible
trigger={null}
style={{ background: t.siderBg }}
>
<Menu
mode="inline"
selectedKeys={getSelectedKeys()}
items={menuItems}
style={{
height: '100%',
borderRight: 0,
padding: '8px',
background: t.siderBg,
}}
/>
</Sider>
{/* 主内容区 */}
<Layout className="app-content-wrapper" style={{ padding: 0, background: t.wrapperBg }}>
{/* 面包屑和标题 */}
<div style={{ padding: '16px 24px 0' }}>
<Breadcrumb className="app-breadcrumb" style={{ marginBottom: 16 }}>
{breadcrumbs.map((crumb, index) => (
<Breadcrumb.Item key={index}>
{index < breadcrumbs.length - 1 ? (
<Link to={crumb.path} style={{ color: t.breadcrumbLink }}>{crumb.title}</Link>
) : (
<span style={{ color: t.breadcrumbCurrent, fontWeight: 500 }}>{crumb.title}</span>
)}
</Breadcrumb.Item>
))}
</Breadcrumb>
<h1 style={{
margin: 0,
fontSize: 22,
fontWeight: 700,
color: t.titleColor,
fontFamily: t.titleFont,
letterSpacing: t.titleLetterSpacing,
}}>
{getPageTitle()}
</h1>
</div>
{/* 内容区 */}
<Content
className="app-content"
style={{
background: t.contentBg,
padding: 24,
borderRadius: 6,
minHeight: 280,
margin: '16px 24px 24px',
border: `1px solid ${t.contentBorder}`,
}}
>
{children}
</Content>
</Layout>
</Layout>
{/* 版本信息弹窗 */}
<VersionModal
visible={versionModalVisible}
onCancel={hideVersionModal}
/>
{/* 快捷键帮助弹窗 */}
<KeyboardShortcutsHelp
visible={shortcutsHelpVisible}
onClose={() => setShortcutsHelpVisible(false)}
/>
</Layout>
</ConfigProvider>
);
};
export default AppLayout;