在前后端开发联调前 的提交20260223
This commit is contained in:
365
apps/admin-web/src/pages/OpsPanel.tsx
Normal file
365
apps/admin-web/src/pages/OpsPanel.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* 运维控制面板页面
|
||||
*
|
||||
* 功能:
|
||||
* - 服务器系统资源概况(CPU / 内存 / 磁盘)
|
||||
* - 各环境服务状态 + 启停重启按钮
|
||||
* - 各环境 Git 状态 + pull / 同步依赖按钮
|
||||
* - 各环境 .env 配置查看(敏感值脱敏)
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
Button,
|
||||
Space,
|
||||
Statistic,
|
||||
Progress,
|
||||
Modal,
|
||||
message,
|
||||
Descriptions,
|
||||
Spin,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Input,
|
||||
} from "antd";
|
||||
import {
|
||||
PlayCircleOutlined,
|
||||
PauseCircleOutlined,
|
||||
ReloadOutlined,
|
||||
CloudDownloadOutlined,
|
||||
SyncOutlined,
|
||||
FileTextOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
DesktopOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type {
|
||||
SystemInfo,
|
||||
ServiceStatus,
|
||||
GitInfo,
|
||||
} from "../api/opsPanel";
|
||||
import {
|
||||
fetchSystemInfo,
|
||||
fetchServicesStatus,
|
||||
fetchGitInfo,
|
||||
startService,
|
||||
stopService,
|
||||
restartService,
|
||||
gitPull,
|
||||
syncDeps,
|
||||
fetchEnvFile,
|
||||
} from "../api/opsPanel";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 工具函数 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** 秒数格式化为 "Xd Xh Xm" */
|
||||
function formatUptime(seconds: number | null): string {
|
||||
if (seconds == null) return "-";
|
||||
const d = Math.floor(seconds / 86400);
|
||||
const h = Math.floor((seconds % 86400) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const parts: string[] = [];
|
||||
if (d > 0) parts.push(`${d}天`);
|
||||
if (h > 0) parts.push(`${h}时`);
|
||||
parts.push(`${m}分`);
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 组件 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const OpsPanel: React.FC = () => {
|
||||
const [system, setSystem] = useState<SystemInfo | null>(null);
|
||||
const [services, setServices] = useState<ServiceStatus[]>([]);
|
||||
const [gitInfos, setGitInfos] = useState<GitInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [actionLoading, setActionLoading] = useState<Record<string, boolean>>({});
|
||||
const [envModalOpen, setEnvModalOpen] = useState(false);
|
||||
const [envModalContent, setEnvModalContent] = useState("");
|
||||
const [envModalTitle, setEnvModalTitle] = useState("");
|
||||
|
||||
// ---- 数据加载 ----
|
||||
|
||||
const loadAll = useCallback(async () => {
|
||||
try {
|
||||
const [sys, svc, git] = await Promise.all([
|
||||
fetchSystemInfo(),
|
||||
fetchServicesStatus(),
|
||||
fetchGitInfo(),
|
||||
]);
|
||||
setSystem(sys);
|
||||
setServices(svc);
|
||||
setGitInfos(git);
|
||||
} catch {
|
||||
message.error("加载运维数据失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadAll();
|
||||
const timer = setInterval(loadAll, 15_000);
|
||||
return () => clearInterval(timer);
|
||||
}, [loadAll]);
|
||||
|
||||
// ---- 操作处理 ----
|
||||
|
||||
const withAction = async (key: string, fn: () => Promise<void>) => {
|
||||
setActionLoading((prev) => ({ ...prev, [key]: true }));
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
setActionLoading((prev) => ({ ...prev, [key]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleStart = (env: string) =>
|
||||
withAction(`start-${env}`, async () => {
|
||||
const r = await startService(env);
|
||||
r.success ? message.success(r.message) : message.warning(r.message);
|
||||
await loadAll();
|
||||
});
|
||||
|
||||
const handleStop = (env: string) =>
|
||||
withAction(`stop-${env}`, async () => {
|
||||
const r = await stopService(env);
|
||||
r.success ? message.success(r.message) : message.warning(r.message);
|
||||
await loadAll();
|
||||
});
|
||||
|
||||
const handleRestart = (env: string) =>
|
||||
withAction(`restart-${env}`, async () => {
|
||||
const r = await restartService(env);
|
||||
r.success ? message.success(r.message) : message.warning(r.message);
|
||||
await loadAll();
|
||||
});
|
||||
|
||||
const handlePull = (env: string) =>
|
||||
withAction(`pull-${env}`, async () => {
|
||||
const r = await gitPull(env);
|
||||
if (r.success) {
|
||||
message.success("拉取成功");
|
||||
Modal.info({ title: `Git Pull - ${env}`, content: <pre style={{ maxHeight: 300, overflow: "auto", fontSize: 12 }}>{r.output}</pre>, width: 600 });
|
||||
} else {
|
||||
message.error("拉取失败");
|
||||
Modal.error({ title: `Git Pull 失败 - ${env}`, content: <pre style={{ maxHeight: 300, overflow: "auto", fontSize: 12 }}>{r.output}</pre>, width: 600 });
|
||||
}
|
||||
await loadAll();
|
||||
});
|
||||
|
||||
const handleSyncDeps = (env: string) =>
|
||||
withAction(`sync-${env}`, async () => {
|
||||
const r = await syncDeps(env);
|
||||
r.success ? message.success("依赖同步完成") : message.error(r.message);
|
||||
});
|
||||
|
||||
const handleViewEnv = async (env: string, label: string) => {
|
||||
try {
|
||||
const r = await fetchEnvFile(env);
|
||||
setEnvModalTitle(`${label} .env 配置`);
|
||||
setEnvModalContent(r.content);
|
||||
setEnvModalOpen(true);
|
||||
} catch {
|
||||
message.error("读取配置文件失败");
|
||||
}
|
||||
};
|
||||
|
||||
// ---- 渲染 ----
|
||||
|
||||
if (loading) {
|
||||
return <Spin size="large" style={{ display: "flex", justifyContent: "center", marginTop: 120 }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={4} style={{ marginBottom: 16 }}>
|
||||
<DesktopOutlined style={{ marginRight: 8 }} />
|
||||
运维控制面板
|
||||
</Title>
|
||||
|
||||
{/* ---- 系统资源 ---- */}
|
||||
{system && (
|
||||
<Card size="small" title="服务器资源" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Statistic title="CPU 使用率" value={system.cpu_percent} suffix="%" />
|
||||
<Progress percent={system.cpu_percent} size="small" status={system.cpu_percent > 80 ? "exception" : "normal"} showInfo={false} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="内存" value={system.memory_used_gb} suffix={`/ ${system.memory_total_gb} GB`} precision={1} />
|
||||
<Progress percent={system.memory_percent} size="small" status={system.memory_percent > 85 ? "exception" : "normal"} showInfo={false} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="磁盘" value={system.disk_used_gb} suffix={`/ ${system.disk_total_gb} GB`} precision={1} />
|
||||
<Progress percent={system.disk_percent} size="small" status={system.disk_percent > 90 ? "exception" : "normal"} showInfo={false} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Text type="secondary" style={{ fontSize: 12, marginTop: 8, display: "block" }}>
|
||||
开机时间:{new Date(system.boot_time).toLocaleString()}
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ---- 服务状态 ---- */}
|
||||
<Card size="small" title="服务状态" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={16}>
|
||||
{services.map((svc) => (
|
||||
<Col span={12} key={svc.env}>
|
||||
<Card
|
||||
size="small"
|
||||
type="inner"
|
||||
title={
|
||||
<Space>
|
||||
{svc.running
|
||||
? <CheckCircleOutlined style={{ color: "#52c41a" }} />
|
||||
: <CloseCircleOutlined style={{ color: "#ff4d4f" }} />}
|
||||
{svc.label}
|
||||
<Tag color={svc.running ? "success" : "error"}>
|
||||
{svc.running ? "运行中" : "已停止"}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={<Tag>:{svc.port}</Tag>}
|
||||
>
|
||||
{svc.running && (
|
||||
<Descriptions size="small" column={3} style={{ marginBottom: 12 }}>
|
||||
<Descriptions.Item label="PID">{svc.pid}</Descriptions.Item>
|
||||
<Descriptions.Item label="运行时长">
|
||||
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
||||
{formatUptime(svc.uptime_seconds)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="内存">{svc.memory_mb ?? "-"} MB</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
<Space>
|
||||
{!svc.running && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlayCircleOutlined />}
|
||||
loading={actionLoading[`start-${svc.env}`]}
|
||||
onClick={() => handleStart(svc.env)}
|
||||
>
|
||||
启动
|
||||
</Button>
|
||||
)}
|
||||
{svc.running && (
|
||||
<>
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<PauseCircleOutlined />}
|
||||
loading={actionLoading[`stop-${svc.env}`]}
|
||||
onClick={() => handleStop(svc.env)}
|
||||
>
|
||||
停止
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={actionLoading[`restart-${svc.env}`]}
|
||||
onClick={() => handleRestart(svc.env)}
|
||||
>
|
||||
重启
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* ---- Git 状态 & 配置 ---- */}
|
||||
<Card size="small" title="代码与配置" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={16}>
|
||||
{gitInfos.map((git) => {
|
||||
const envCfg = services.find((s) => s.env === git.env);
|
||||
const label = envCfg?.label ?? git.env;
|
||||
return (
|
||||
<Col span={12} key={git.env}>
|
||||
<Card size="small" type="inner" title={label}>
|
||||
<Descriptions size="small" column={1} style={{ marginBottom: 12 }}>
|
||||
<Descriptions.Item label="分支">
|
||||
<Tag color="blue">{git.branch}</Tag>
|
||||
{git.has_local_changes && (
|
||||
<Tooltip title="工作区有未提交的变更">
|
||||
<Tag color="warning">有变更</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最新提交">
|
||||
<Text code style={{ fontSize: 12 }}>{git.last_commit_hash}</Text>
|
||||
<Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
|
||||
{git.last_commit_message}
|
||||
</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提交时间">
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{git.last_commit_time}</Text>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CloudDownloadOutlined />}
|
||||
loading={actionLoading[`pull-${git.env}`]}
|
||||
onClick={() => handlePull(git.env)}
|
||||
>
|
||||
Git Pull
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<SyncOutlined />}
|
||||
loading={actionLoading[`sync-${git.env}`]}
|
||||
onClick={() => handleSyncDeps(git.env)}
|
||||
>
|
||||
同步依赖
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={() => handleViewEnv(git.env, label)}
|
||||
>
|
||||
查看配置
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* ---- 配置查看弹窗 ---- */}
|
||||
<Modal
|
||||
title={envModalTitle}
|
||||
open={envModalOpen}
|
||||
onCancel={() => setEnvModalOpen(false)}
|
||||
footer={null}
|
||||
width={700}
|
||||
>
|
||||
<TextArea
|
||||
value={envModalContent}
|
||||
readOnly
|
||||
autoSize={{ minRows: 10, maxRows: 30 }}
|
||||
style={{ fontFamily: "monospace", fontSize: 12 }}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpsPanel;
|
||||
Reference in New Issue
Block a user