Files
Neo-ZQYY/apps/admin-web/src/App.tsx

212 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 主布局与路由配置。
*
* - Ant Design LayoutSider + Content + Footer状态栏
* - react-router-dom6 个功能页面路由 + 登录页路由
* - 路由守卫:未登录重定向到登录页
*/
import React, { useEffect, useState, useCallback } from "react";
import { Routes, Route, Navigate, useNavigate, useLocation } from "react-router-dom";
import { Layout, Menu, Spin, Space, Typography, Tag, Button, Tooltip } from "antd";
import {
SettingOutlined,
UnorderedListOutlined,
ToolOutlined,
DatabaseOutlined,
DashboardOutlined,
FileTextOutlined,
LogoutOutlined,
DesktopOutlined,
} from "@ant-design/icons";
import type { MenuProps } from "antd";
import { useAuthStore } from "./store/authStore";
import { useBusinessDayStore } from "./store/businessDayStore";
import { fetchQueue } from "./api/execution";
import type { QueuedTask } from "./types";
import Login from "./pages/Login";
import TaskConfig from "./pages/TaskConfig";
import TaskManager from "./pages/TaskManager";
import EnvConfig from "./pages/EnvConfig";
import DBViewer from "./pages/DBViewer";
import ETLStatus from "./pages/ETLStatus";
import LogViewer from "./pages/LogViewer";
import OpsPanel from "./pages/OpsPanel";
const { Sider, Content, Footer } = Layout;
const { Text } = Typography;
/* ------------------------------------------------------------------ */
/* 侧边栏导航配置 */
/* ------------------------------------------------------------------ */
const NAV_ITEMS: MenuProps["items"] = [
{ key: "/", icon: <SettingOutlined />, label: "任务配置" },
{ key: "/task-manager", icon: <UnorderedListOutlined />, label: "任务管理" },
{ key: "/etl-status", icon: <DashboardOutlined />, label: "ETL 状态" },
{ key: "/db-viewer", icon: <DatabaseOutlined />, label: "数据库" },
{ key: "/log-viewer", icon: <FileTextOutlined />, label: "日志" },
{ key: "/env-config", icon: <ToolOutlined />, label: "环境配置" },
{ key: "/ops-panel", icon: <DesktopOutlined />, label: "运维面板" },
];
/* ------------------------------------------------------------------ */
/* 路由守卫 */
/* ------------------------------------------------------------------ */
const PrivateRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
};
/* ------------------------------------------------------------------ */
/* 主布局 */
/* ------------------------------------------------------------------ */
const AppLayout: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const logout = useAuthStore((s) => s.logout);
const [runningTask, setRunningTask] = useState<QueuedTask | null>(null);
const pollQueue = useCallback(async () => {
try {
const queue = await fetchQueue();
const running = queue.find((t) => t.status === "running") ?? null;
setRunningTask(running);
} catch {
// 网络异常时不更新状态
}
}, []);
useEffect(() => {
pollQueue();
const timer = setInterval(pollQueue, 5_000);
return () => clearInterval(timer);
}, [pollQueue]);
const onMenuClick: MenuProps["onClick"] = ({ key }) => { navigate(key); };
const handleLogout = () => {
logout();
navigate("/login", { replace: true });
};
return (
<Layout style={{ minHeight: "100vh" }}>
<Sider
collapsible
style={{ display: "flex", flexDirection: "column" }}
>
<div
style={{
height: 48,
margin: "12px 16px",
color: "#fff",
fontWeight: 700,
fontSize: 18,
textAlign: "center",
lineHeight: "48px",
whiteSpace: "nowrap",
overflow: "hidden",
letterSpacing: 1,
}}
>
NeoZQYY
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
items={NAV_ITEMS}
onClick={onMenuClick}
/>
<div style={{ flex: 1 }} />
<div style={{ padding: "12px 16px" }}>
<Tooltip title="退出登录">
<Button
type="text" icon={<LogoutOutlined />}
onClick={handleLogout}
style={{ color: "rgba(255,255,255,0.65)", width: "100%" }}
>
退
</Button>
</Tooltip>
</div>
</Sider>
<Layout>
<Content style={{ margin: 16, minHeight: 280 }}>
<Routes>
<Route path="/" element={<TaskConfig />} />
<Route path="/task-manager" element={<TaskManager />} />
<Route path="/env-config" element={<EnvConfig />} />
<Route path="/db-viewer" element={<DBViewer />} />
<Route path="/etl-status" element={<ETLStatus />} />
<Route path="/log-viewer" element={<LogViewer />} />
<Route path="/ops-panel" element={<OpsPanel />} />
</Routes>
</Content>
<Footer
style={{
textAlign: "center",
padding: "6px 16px",
background: "#fafafa",
borderTop: "1px solid #f0f0f0",
}}
>
{runningTask ? (
<Space size={8}>
<Spin size="small" />
<Text></Text>
<Tag color="processing">{runningTask.config.flow}</Tag>
<Text type="secondary" style={{ fontSize: 12 }}>
{runningTask.config.tasks.slice(0, 3).join(", ")}
{runningTask.config.tasks.length > 3 && ` +${runningTask.config.tasks.length - 3}`}
</Text>
</Space>
) : (
<Text type="secondary" style={{ fontSize: 12 }}></Text>
)}
</Footer>
</Layout>
</Layout>
);
};
/* ------------------------------------------------------------------ */
/* 根组件 */
/* ------------------------------------------------------------------ */
const App: React.FC = () => {
const hydrate = useAuthStore((s) => s.hydrate);
const initBusinessDay = useBusinessDayStore((s) => s.init);
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
hydrate();
setHydrated(true);
// 启动时请求一次营业日配置,降级策略在 store 内部处理
initBusinessDay();
}, [hydrate, initBusinessDay]);
/* hydrate 完成前不渲染路由,避免 PrivateRoute 误判跳转到 /login */
if (!hydrated) return <Spin style={{ display: "flex", justifyContent: "center", marginTop: 120 }} />;
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/*"
element={
<PrivateRoute>
<AppLayout />
</PrivateRoute>
}
/>
</Routes>
);
};
export default App;