safety-eval-service-frontend/src/pages/Container/Layout/index.jsx

435 lines
12 KiB
React
Raw Normal View History

2026-06-22 13:48:12 +08:00
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>
);
}