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

435 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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