435 lines
12 KiB
React
435 lines
12 KiB
React
|
|
import React, {
|
|||
|
|
useState,
|
|||
|
|
useCallback,
|
|||
|
|
useMemo,
|
|||
|
|
useEffect,
|
|||
|
|
} from "react";
|
|||
|
|
import { Layout, Menu, Breadcrumb, Tabs, Button, Dropdown, Space, Typography } from "antd";
|
|||
|
|
import {
|
|||
|
|
ReloadOutlined,
|
|||
|
|
CloseOutlined,
|
|||
|
|
CloseCircleOutlined,
|
|||
|
|
DownOutlined,
|
|||
|
|
MenuFoldOutlined,
|
|||
|
|
MenuUnfoldOutlined,
|
|||
|
|
} from "@ant-design/icons";
|
|||
|
|
import menuItems, {
|
|||
|
|
findMenuPath,
|
|||
|
|
getPageLabel,
|
|||
|
|
getOpenKeys,
|
|||
|
|
getSelectedKeys,
|
|||
|
|
} from "./menuConfig";
|
|||
|
|
|
|||
|
|
const { Header, Sider, Content } = Layout;
|
|||
|
|
const { Text } = Typography;
|
|||
|
|
|
|||
|
|
const TABS_KEY = "sim_layout_tabs";
|
|||
|
|
const ACTIVE_KEY = "sim_layout_active";
|
|||
|
|
|
|||
|
|
/* ---- sessionStorage 持久化 ---- */
|
|||
|
|
function loadState() {
|
|||
|
|
try {
|
|||
|
|
const tabs = JSON.parse(sessionStorage.getItem(TABS_KEY) || "[]");
|
|||
|
|
const activeKey = sessionStorage.getItem(ACTIVE_KEY) || "";
|
|||
|
|
return { tabs, activeKey };
|
|||
|
|
} catch {
|
|||
|
|
return { tabs: [], activeKey: "" };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function saveState(tabs, activeKey) {
|
|||
|
|
sessionStorage.setItem(TABS_KEY, JSON.stringify(tabs));
|
|||
|
|
sessionStorage.setItem(ACTIVE_KEY, activeKey || "");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function SimulatedLayout({ children }) {
|
|||
|
|
const [collapsed, setCollapsed] = useState(false);
|
|||
|
|
const [state, setState] = useState(loadState);
|
|||
|
|
const currentPath = window.location.pathname;
|
|||
|
|
|
|||
|
|
// 页面加载时自动将当前路径加入标签
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (currentPath && !currentPath.endsWith("/container/")) {
|
|||
|
|
addTab(currentPath);
|
|||
|
|
}
|
|||
|
|
}, []); // 仅挂载时执行一次
|
|||
|
|
|
|||
|
|
/* ---- 标签操作 ---- */
|
|||
|
|
const addTab = useCallback((path) => {
|
|||
|
|
setState((prev) => {
|
|||
|
|
if (prev.tabs.find((t) => t.key === path)) {
|
|||
|
|
// 已存在则仅更新 activeKey
|
|||
|
|
if (prev.activeKey !== path) {
|
|||
|
|
saveState(prev.tabs, path);
|
|||
|
|
return { ...prev, activeKey: path };
|
|||
|
|
}
|
|||
|
|
return prev;
|
|||
|
|
}
|
|||
|
|
const newTabs = [...prev.tabs, { key: path, label: getPageLabel(path) }];
|
|||
|
|
saveState(newTabs, path);
|
|||
|
|
return { tabs: newTabs, activeKey: path };
|
|||
|
|
});
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const navigateTo = useCallback((path) => {
|
|||
|
|
if (path !== currentPath) {
|
|||
|
|
window.location.href = path;
|
|||
|
|
}
|
|||
|
|
}, [currentPath]);
|
|||
|
|
|
|||
|
|
const handleCloseTab = useCallback((targetKey) => {
|
|||
|
|
setState((prev) => {
|
|||
|
|
const idx = prev.tabs.findIndex((t) => t.key === targetKey);
|
|||
|
|
const newTabs = prev.tabs.filter((t) => t.key !== targetKey);
|
|||
|
|
if (newTabs.length === 0) {
|
|||
|
|
saveState([], "");
|
|||
|
|
return { tabs: [], activeKey: "" };
|
|||
|
|
}
|
|||
|
|
let newActive = prev.activeKey;
|
|||
|
|
if (targetKey === prev.activeKey) {
|
|||
|
|
newActive = newTabs[Math.max(0, idx - 1)]?.key || newTabs[0]?.key;
|
|||
|
|
navigateTo(newActive);
|
|||
|
|
}
|
|||
|
|
saveState(newTabs, newActive);
|
|||
|
|
return { tabs: newTabs, activeKey: newActive };
|
|||
|
|
});
|
|||
|
|
}, [navigateTo]);
|
|||
|
|
|
|||
|
|
const handleCloseOthers = useCallback((targetKey) => {
|
|||
|
|
setState((prev) => {
|
|||
|
|
const target = prev.tabs.find((t) => t.key === targetKey);
|
|||
|
|
const newTabs = target ? [target] : [];
|
|||
|
|
saveState(newTabs, targetKey);
|
|||
|
|
if (targetKey !== currentPath) navigateTo(targetKey);
|
|||
|
|
return { tabs: newTabs, activeKey: targetKey };
|
|||
|
|
});
|
|||
|
|
}, [currentPath, navigateTo]);
|
|||
|
|
|
|||
|
|
const handleCloseRight = useCallback((targetKey) => {
|
|||
|
|
setState((prev) => {
|
|||
|
|
const idx = prev.tabs.findIndex((t) => t.key === targetKey);
|
|||
|
|
if (idx === -1) return prev;
|
|||
|
|
const newTabs = prev.tabs.slice(0, idx + 1);
|
|||
|
|
saveState(newTabs, targetKey);
|
|||
|
|
return { tabs: newTabs, activeKey: targetKey };
|
|||
|
|
});
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const handleCloseAll = useCallback(() => {
|
|||
|
|
saveState([], "");
|
|||
|
|
setState({ tabs: [], activeKey: "" });
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const handleRefresh = useCallback(() => {
|
|||
|
|
window.location.reload();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
/* ---- 菜单事件 ---- */
|
|||
|
|
const handleMenuClick = useCallback(
|
|||
|
|
({ key }) => {
|
|||
|
|
navigateTo(key);
|
|||
|
|
},
|
|||
|
|
[navigateTo],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const handleTabChange = useCallback(
|
|||
|
|
(key) => {
|
|||
|
|
navigateTo(key);
|
|||
|
|
},
|
|||
|
|
[navigateTo],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const handleTabEdit = useCallback(
|
|||
|
|
(targetKey, action) => {
|
|||
|
|
if (action === "remove") handleCloseTab(targetKey);
|
|||
|
|
},
|
|||
|
|
[handleCloseTab],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
/* ---- 面包屑 ---- */
|
|||
|
|
const breadcrumbItems = useMemo(() => {
|
|||
|
|
const path = findMenuPath(currentPath);
|
|||
|
|
return path.map((item, idx) => ({
|
|||
|
|
title:
|
|||
|
|
idx === path.length - 1 ? (
|
|||
|
|
item.label
|
|||
|
|
) : (
|
|||
|
|
<a onClick={() => handleMenuClick({ key: item.key })}>{item.label}</a>
|
|||
|
|
),
|
|||
|
|
key: item.key,
|
|||
|
|
}));
|
|||
|
|
}, [currentPath, handleMenuClick]);
|
|||
|
|
|
|||
|
|
/* ---- 菜单选中/展开 ---- */
|
|||
|
|
const selectedKeys = useMemo(() => getSelectedKeys(currentPath), [currentPath]);
|
|||
|
|
const defaultOpenKeys = useMemo(() => getOpenKeys(currentPath), [currentPath]);
|
|||
|
|
|
|||
|
|
/* ---- 标签页右键菜单 ---- */
|
|||
|
|
const buildContextMenu = useCallback(
|
|||
|
|
(tabKey) => {
|
|||
|
|
const { tabs } = state;
|
|||
|
|
const isOnlyOne = tabs.length <= 1;
|
|||
|
|
const idx = tabs.findIndex((t) => t.key === tabKey);
|
|||
|
|
const hasRight = idx >= 0 && idx < tabs.length - 1;
|
|||
|
|
return {
|
|||
|
|
items: [
|
|||
|
|
{
|
|||
|
|
key: "refresh",
|
|||
|
|
icon: <ReloadOutlined />,
|
|||
|
|
label: "刷新当前标签",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "close",
|
|||
|
|
icon: <CloseOutlined />,
|
|||
|
|
label: "关闭当前标签",
|
|||
|
|
disabled: isOnlyOne,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "close-others",
|
|||
|
|
icon: <CloseCircleOutlined />,
|
|||
|
|
label: "关闭其他标签",
|
|||
|
|
disabled: isOnlyOne,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "close-right",
|
|||
|
|
icon: <CloseOutlined />,
|
|||
|
|
label: "关闭右侧标签",
|
|||
|
|
disabled: !hasRight,
|
|||
|
|
},
|
|||
|
|
{ type: "divider" },
|
|||
|
|
{
|
|||
|
|
key: "close-all",
|
|||
|
|
icon: <CloseCircleOutlined />,
|
|||
|
|
label: "关闭所有标签",
|
|||
|
|
disabled: tabs.length === 0,
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
onClick: ({ key }) => {
|
|||
|
|
switch (key) {
|
|||
|
|
case "refresh":
|
|||
|
|
handleRefresh();
|
|||
|
|
break;
|
|||
|
|
case "close":
|
|||
|
|
handleCloseTab(tabKey);
|
|||
|
|
break;
|
|||
|
|
case "close-others":
|
|||
|
|
handleCloseOthers(tabKey);
|
|||
|
|
break;
|
|||
|
|
case "close-right":
|
|||
|
|
handleCloseRight(tabKey);
|
|||
|
|
break;
|
|||
|
|
case "close-all":
|
|||
|
|
handleCloseAll();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
[state, handleRefresh, handleCloseTab, handleCloseOthers, handleCloseRight, handleCloseAll],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
/* ---- 标签 label 渲染(带右键菜单) ---- */
|
|||
|
|
const renderTabLabel = useCallback(
|
|||
|
|
(tab) => (
|
|||
|
|
<Dropdown menu={buildContextMenu(tab.key)} trigger={["contextMenu"]}>
|
|||
|
|
<span style={{ display: "inline-block", padding: "0 8px", userSelect: "none" }}>
|
|||
|
|
{tab.label}
|
|||
|
|
</span>
|
|||
|
|
</Dropdown>
|
|||
|
|
),
|
|||
|
|
[buildContextMenu],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const { tabs, activeKey } = state;
|
|||
|
|
const siderWidth = collapsed ? 64 : 220;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Layout style={{ minHeight: "100vh" }}>
|
|||
|
|
{/* ---- 侧边栏 ---- */}
|
|||
|
|
<Sider
|
|||
|
|
collapsible
|
|||
|
|
collapsed={collapsed}
|
|||
|
|
onCollapse={setCollapsed}
|
|||
|
|
theme="dark"
|
|||
|
|
width={220}
|
|||
|
|
style={{
|
|||
|
|
overflow: "auto",
|
|||
|
|
height: "100vh",
|
|||
|
|
position: "fixed",
|
|||
|
|
left: 0,
|
|||
|
|
top: 0,
|
|||
|
|
bottom: 0,
|
|||
|
|
zIndex: 10,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
height: 48,
|
|||
|
|
display: "flex",
|
|||
|
|
alignItems: "center",
|
|||
|
|
justifyContent: "center",
|
|||
|
|
color: "#fff",
|
|||
|
|
fontWeight: 700,
|
|||
|
|
fontSize: collapsed ? 15 : 17,
|
|||
|
|
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
|||
|
|
letterSpacing: 1,
|
|||
|
|
flexShrink: 0,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{collapsed ? "证照" : "证照管理系统"}
|
|||
|
|
</div>
|
|||
|
|
<Menu
|
|||
|
|
theme="dark"
|
|||
|
|
mode="inline"
|
|||
|
|
selectedKeys={selectedKeys}
|
|||
|
|
defaultOpenKeys={defaultOpenKeys}
|
|||
|
|
onClick={handleMenuClick}
|
|||
|
|
items={menuItems}
|
|||
|
|
style={{ borderRight: 0 }}
|
|||
|
|
/>
|
|||
|
|
</Sider>
|
|||
|
|
|
|||
|
|
{/* ---- 右侧主体 ---- */}
|
|||
|
|
<Layout
|
|||
|
|
style={{
|
|||
|
|
marginLeft: siderWidth,
|
|||
|
|
transition: "margin-left 0.2s",
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{/* ---- Header + 面包屑 + 操作按钮 ---- */}
|
|||
|
|
<Header
|
|||
|
|
style={{
|
|||
|
|
background: "#fff",
|
|||
|
|
padding: "0 16px",
|
|||
|
|
display: "flex",
|
|||
|
|
alignItems: "center",
|
|||
|
|
justifyContent: "space-between",
|
|||
|
|
borderBottom: "1px solid #f0f0f0",
|
|||
|
|
height: 48,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Space>
|
|||
|
|
<Button
|
|||
|
|
type="text"
|
|||
|
|
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|||
|
|
onClick={() => setCollapsed(!collapsed)}
|
|||
|
|
/>
|
|||
|
|
<Breadcrumb items={breadcrumbItems} />
|
|||
|
|
</Space>
|
|||
|
|
<Space>
|
|||
|
|
<Button
|
|||
|
|
type="text"
|
|||
|
|
icon={<ReloadOutlined />}
|
|||
|
|
onClick={handleRefresh}
|
|||
|
|
>
|
|||
|
|
刷新
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
type="text"
|
|||
|
|
icon={<CloseCircleOutlined />}
|
|||
|
|
onClick={handleCloseAll}
|
|||
|
|
disabled={tabs.length === 0}
|
|||
|
|
>
|
|||
|
|
关闭所有
|
|||
|
|
</Button>
|
|||
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|||
|
|
模拟布局·Dev
|
|||
|
|
</Text>
|
|||
|
|
</Space>
|
|||
|
|
</Header>
|
|||
|
|
|
|||
|
|
{/* ---- 标签栏 ---- */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
background: "#fff",
|
|||
|
|
borderBottom: "1px solid #f0f0f0",
|
|||
|
|
padding: "4px 8px 0",
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{tabs.length > 0 ? (
|
|||
|
|
<Tabs
|
|||
|
|
type="editable-card"
|
|||
|
|
hideAdd
|
|||
|
|
activeKey={activeKey}
|
|||
|
|
onChange={handleTabChange}
|
|||
|
|
onEdit={handleTabEdit}
|
|||
|
|
size="small"
|
|||
|
|
style={{ marginBottom: 0 }}
|
|||
|
|
items={tabs.map((tab) => ({
|
|||
|
|
key: tab.key,
|
|||
|
|
label: renderTabLabel(tab),
|
|||
|
|
closable: true,
|
|||
|
|
}))}
|
|||
|
|
tabBarExtraContent={
|
|||
|
|
<Dropdown
|
|||
|
|
menu={{
|
|||
|
|
items: [
|
|||
|
|
{
|
|||
|
|
key: "refresh",
|
|||
|
|
icon: <ReloadOutlined />,
|
|||
|
|
label: "刷新当前标签",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "close-others",
|
|||
|
|
icon: <CloseOutlined />,
|
|||
|
|
label: "关闭其他标签",
|
|||
|
|
disabled: tabs.length <= 1,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "close-all",
|
|||
|
|
icon: <CloseCircleOutlined />,
|
|||
|
|
label: "关闭所有标签",
|
|||
|
|
disabled: tabs.length === 0,
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
onClick: ({ key }) => {
|
|||
|
|
switch (key) {
|
|||
|
|
case "refresh":
|
|||
|
|
handleRefresh();
|
|||
|
|
break;
|
|||
|
|
case "close-others":
|
|||
|
|
handleCloseOthers(activeKey);
|
|||
|
|
break;
|
|||
|
|
case "close-all":
|
|||
|
|
handleCloseAll();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
}}
|
|||
|
|
placement="bottomRight"
|
|||
|
|
>
|
|||
|
|
<Button type="text" size="small" icon={<DownOutlined />} />
|
|||
|
|
</Dropdown>
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
height: 36,
|
|||
|
|
display: "flex",
|
|||
|
|
alignItems: "center",
|
|||
|
|
paddingLeft: 8,
|
|||
|
|
color: "#bbb",
|
|||
|
|
fontSize: 12,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
暂无打开的标签页,请从左侧菜单选择页面
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* ---- 内容区 ---- */}
|
|||
|
|
<Content
|
|||
|
|
style={{
|
|||
|
|
margin: 0,
|
|||
|
|
minHeight: 280,
|
|||
|
|
position: "relative",
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{children}
|
|||
|
|
</Content>
|
|||
|
|
</Layout>
|
|||
|
|
</Layout>
|
|||
|
|
);
|
|||
|
|
}
|