| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618 |
- import React, { useState, useCallback, ReactNode } from 'react';
- import { createRoot } from 'react-dom/client';
- import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
- import { Pie, Column, Line } from '@ant-design/plots';
- import { FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons';
- import {
- Card
- } from 'antd';
- import { AlarmDeviceData } from '../share/monitorTypes.ts';
- import { queryFns } from './api.ts';
- import { ThreeJSRoom } from './components_three.tsx';
- import { GlobalConfig } from "../share/types.ts";
- const queryClient = new QueryClient();
- // 声明全局配置对象类型
- declare global {
- interface Window {
- CONFIG?: GlobalConfig;
- }
- }
- interface ServerMonitorChartsProps {
- type?: 'pie' | 'column' | 'line' | 'pie2';
- }
- const pushStateAndTrigger = (url: string, target: string) => {
- window.history.pushState({}, '', url);
- window.dispatchEvent(new Event('popstate'));
- }
- // 统一的链接处理函数
- const handleNavigate = (url: string) => {
- // 判断是否在iframe中
- const isInIframe = window.self !== window.top;
- if (isInIframe) {
- pushStateAndTrigger(url, 'top');
- } else {
- window.open(url, '_blank');
- }
- };
- function AlarmDeviceTable() {
- const { data = [], isLoading } = useQuery({
- queryKey: ['topAlarmDevices'],
- queryFn: queryFns.fetchTopAlarmDevices,
- refetchInterval: 30000
- });
- if (isLoading) {
- return (
- <div className="flex items-center justify-center h-full">
- <span className="text-[#00dff9]">加载中...</span>
- </div>
- );
- }
- return (
- <div className="flex flex-col h-full text-sm">
- {/* 表头 */}
- <div className="grid grid-cols-[80px_100px_1fr] gap-2 px-4 py-2 text-gray-300 border-b border-[#00dff9]/20">
- <div className="text-center">排名</div>
- <div className="text-center">告警次数</div>
- <div className="text-left">点位名称</div>
- </div>
- {/* 表格内容 */}
- <div className="flex-1 overflow-y-auto">
- {data.map((item: AlarmDeviceData) => (
- <div
- key={item.rank}
- className="grid grid-cols-[80px_100px_1fr] gap-2 px-4 py-2 hover:bg-[#001529] transition-colors duration-200 border-b border-[#00dff9]/10"
- >
- <div className="text-center text-[#00dff9]">{item.rank}</div>
- <div className="text-center text-yellow-400">{item.alarmCount}</div>
- <a
- href="/admin/alarms"
- className="text-left text-[#00dff9] hover:text-white truncate"
- title={item.deviceName}
- >
- {item.deviceName}
- </a>
- </div>
- ))}
- {data.length === 0 && (
- <div className="flex items-center justify-center h-full text-gray-400">
- 暂无数据
- </div>
- )}
- </div>
- </div>
- );
- }
- function CustomCard({ title, children, className = '', bodyStyle = {}, onClick }: {
- title?: string;
- children: ReactNode;
- className?: string;
- bodyStyle?: React.CSSProperties;
- onClick?: () => void;
- }) {
- return (
- <div
- className={`relative bg-[#001529] border border-[#00dff9] shadow-[0_0_10px_rgba(0,223,249,0.3)] hover:shadow-[0_0_15px_rgba(0,223,249,0.4)] transition-shadow rounded-md ${className}`}
- onClick={onClick}
- >
- {title && (
- <div className="absolute -top-3 left-4 px-2 bg-[#001529] text-[#00dff9] text-sm">
- {title}
- </div>
- )}
- <div style={bodyStyle} className="p-4">
- {children}
- </div>
- </div>
- );
- }
- function MetricCards() {
- const { data: metrics = {
- totalDevices: 0,
- onlineDevices: 0,
- offlineDevices: 0,
- onlineRate: "0.00",
- riskLevel: { level: '健康', color: '#52c41a' }
- }, isLoading } = useQuery({
- queryKey: ['deviceMetrics'],
- queryFn: queryFns.fetchDeviceMetrics,
- refetchInterval: 30000,
- refetchIntervalInBackground: true
- });
- const metricConfigs = [
- {
- title: "设备数",
- value: metrics.totalDevices,
- link: "/admin/alarm/manage"
- },
- {
- title: "正常数",
- value: metrics.onlineDevices,
- link: "/admin/alarm/manage"
- },
- {
- title: "在线率",
- value: `${metrics.onlineRate}%`,
- link: "/admin/device/rate"
- },
- {
- title: "异常数",
- value: metrics.offlineDevices,
- link: "/admin/alert/manage"
- },
- {
- title: "风险等级",
- value: metrics.riskLevel.level,
- color: metrics.riskLevel.color,
- link: undefined
- }
- ];
- return (
- <>
- {metricConfigs.map((metric, index) => (
- <div
- key={index}
- onClick={() => metric.link && handleNavigate(metric.link)}
- className={metric.link ? "cursor-pointer" : undefined}
- >
- <Card
- className="bg-[#001529] border border-[#00dff9] shadow-[0_0_10px_rgba(0,223,249,0.3)] hover:shadow-[0_0_15px_rgba(0,223,249,0.4)] transition-shadow relative before:content-[''] before:absolute before:left-[3px] before:top-[3px] before:h-[calc(100%-6px)] before:w-1 before:bg-gradient-to-b before:from-yellow-500 before:via-yellow-400 before:to-transparent rounded-md"
- bordered={false}
- styles={{
- body: {
- padding: "16px",
- paddingLeft: "24px",
- display: "flex",
- alignItems: "center",
- justifyContent: "space-between",
- },
- }}
- >
- <span className="text-gray-300 text-base">{metric.title}</span>
- <span className="text-3xl" style={{ color: metric.color || '#00dff9' }}>
- {isLoading ? "-" : metric.value}
- </span>
- </Card>
- </div>
- ))}
- </>
- );
- }
- function ServerMonitorCharts({ type = 'pie' }: ServerMonitorChartsProps) {
- // 资产分类数据
- const { data: categoryData } = useQuery({
- queryKey: ['zichanCategory'],
- queryFn: queryFns.fetchCategoryData,
- enabled: type === 'pie'
- });
- // 在线率变化数据
- const { data: onlineRateData } = useQuery({
- queryKey: ['zichanOnlineRate'],
- queryFn: queryFns.fetchOnlineRateData,
- enabled: type === 'column'
- });
- // 资产流转状态数据
- const { data: stateData } = useQuery({
- queryKey: ['zichanState'],
- queryFn: queryFns.fetchStateData,
- enabled: type === 'pie2'
- });
- // 告警数据变化
- const { data: alarmData } = useQuery({
- queryKey: ['pingAlarm'],
- queryFn: queryFns.fetchAlarmData,
- enabled: type === 'line'
- });
- const renderChart = () => {
- switch (type) {
- case 'pie':
- return (
- <Pie
- data={categoryData || []}
- angleField="设备数"
- colorField="设备分类"
- radius={0.8}
- label={{
- position: 'outside',
- text: ({ 设备分类, 设备数, 百分比, percent }: { 设备分类: string, 设备数: number, 百分比: string, percent: number }) => {
- // 只有占比超过5%的项才显示标签
- if (percent < 0.05) return null;
- return `${设备分类}\n(${设备数})`;
- },
- style: {
- fill: '#fff',
- fontSize: 12,
- fontWeight: 500,
- },
- transform: [{ type: 'overlapDodgeY' }],
- }}
- theme={{
- colors10: ['#36cfc9', '#ff4d4f', '#ffa940', '#73d13d', '#4096ff'],
- }}
- legend={false}
- autoFit={true}
- interaction={{
- tooltip: {
- render: (_: any, { items, title }: { items: any[], title: string }) => {
- if (!items || items.length === 0) return '';
-
- // 获取当前选中项的数据
- const item = items[0];
-
- // 根据value找到对应的完整数据项
- const fullData = categoryData?.find(d => d['设备数'] === item.value);
- if (!fullData) return '';
-
- return `<div class="bg-white p-2 rounded">
- <div class="flex items-center">
- <div class="w-3 h-3 rounded-full mr-2" style="background:${item.color}"></div>
- <span class="font-semibold text-gray-900">${fullData['设备分类']}</span>
- </div>
- <p class="text-sm text-gray-800">数量: ${item.value}</p>
- <p class="text-sm text-gray-800">占比: ${fullData['百分比']}%</p>
- </div>`;
- }
- }
- }}
- />
- );
- case 'column':
- return (
- <Column
- data={onlineRateData || []}
- xField="time_interval"
- yField="total_devices"
- color="#36cfc9"
- label={{
- position: 'top',
- style: {
- fill: '#fff',
- },
- text: (items: any) => {
- let content = items['time_interval'];
- content += `\n(${(items['total_devices'])})`;
- return content;
- },
- }}
- xAxis={{
- label: {
- style: {
- fill: '#fff',
- },
- },
- }}
- yAxis={{
- label: {
- style: {
- fill: '#fff',
- },
- },
- }}
- autoFit={true}
- interaction={{
- tooltip:false
- }}
- />
- );
- case 'line':
- return (
- <Line
- data={alarmData || []}
- xField="time_interval"
- yField="total_devices"
- smooth={true}
- color="#36cfc9"
- label={{
- position: 'top',
- style: {
- fill: '#fff',
- fontSize: 12,
- fontWeight: 500,
- },
- text: (items: any) => {
- const value = items['total_devices'];
-
- // if (value === 0) return null;
-
- // const maxValue = Math.max(...(alarmData || []).map(item => item.total_devices));
-
- // if (value < maxValue * 0.3 && alarmData && alarmData.length > 8) return null;
-
- return `${items['time_interval']}\n(${value})`;
- },
- transform: [{ type: 'overlapDodgeY' }],
- }}
- xAxis={{
- label: {
- style: {
- fill: '#fff',
- },
- autoHide: true,
- autoRotate: true,
- },
- }}
- yAxis={{
- label: {
- style: {
- fill: '#fff',
- },
- },
- }}
- autoFit={true}
- interaction={{
- tooltip: {
- render: (_: any, { items, title }: { items: any[], title: string }) => {
- if (!items || items.length === 0) return '';
-
- // 获取当前选中项的数据
- const item = items[0];
-
- // 根据value找到对应的完整数据项
- const fullData = alarmData?.find(d => d.total_devices === item.value);
- if (!fullData) return '';
-
- return `<div class="bg-white p-2 rounded">
- <div class="flex items-center">
- <div class="w-3 h-3 rounded-full mr-2" style="background:${item.color}"></div>
- <span class="font-semibold text-gray-900">${fullData.time_interval}</span>
- </div>
- <p class="text-sm text-gray-800">数量: ${item.value}</p>
- </div>`;
- }
- }
- }}
- />
- );
- case 'pie2':
- return (
- <Pie
- data={stateData || []}
- angleField="设备数"
- colorField="资产流转"
- radius={0.9}
- innerRadius={0.8}
- label={{
- position: 'outside',
- text: ({ 资产流转, 设备数, 百分比, percent }: { 资产流转: string, 设备数: number, 百分比: string, percent: number }) => {
- // 只有占比超过5%的项才显示标签
- if (percent < 0.05) return null;
- return `${资产流转}\n(${设备数})`;
- },
- style: {
- fill: '#fff',
- fontSize: 12,
- fontWeight: 500,
- },
- transform: [{ type: 'overlapDodgeY' }],
- }}
- theme={{
- colors10: ['#36cfc9', '#ff4d4f', '#ffa940', '#73d13d', '#4096ff'],
- }}
- legend={{
- color: {
- itemLabelFill: '#fff',
- }
- }}
- autoFit={true}
- interaction={{
- tooltip: {
- render: (_: any, { items, title }: { items: any[], title: string }) => {
- if (!items || items.length === 0) return '';
-
- // 获取当前选中项的数据
- const item = items[0];
-
- // 根据value找到对应的完整数据项
- const fullData = stateData?.find(d => d['设备数'] === item.value);
- if (!fullData) return '';
-
- return `<div class="bg-white p-2 rounded">
- <div class="flex items-center">
- <div class="w-3 h-3 rounded-full mr-2" style="background:${item.color}"></div>
- <span class="font-semibold text-gray-900">${fullData['资产流转']}</span>
- </div>
- <p class="text-sm text-gray-800">数量: ${item.value}</p>
- <p class="text-sm text-gray-800">占比: ${fullData['百分比']}%</p>
- </div>`;
- }
- }
- }}
- />
- );
- }
- };
- return (
- <div className="w-full h-full">
- {renderChart()}
- </div>
- );
- }
- function PageTitle() {
- return (
- <div className="relative w-full">
- {/* 背景图片 */}
- <div className="w-full h-[60px]">
- <img
- src="/client/big/title-bg.png"
- alt="title background"
- className="w-full h-full object-fill"
- />
- </div>
- </div>
- );
- }
- function DataCenter() {
- const [isFullscreen, setIsFullscreen] = useState(false);
- const toggleFullscreen = useCallback(() => {
- if (!document.fullscreenElement) {
- document.documentElement.requestFullscreen();
- setIsFullscreen(true);
- } else {
- document.exitFullscreen().then(() => {
- setIsFullscreen(false);
- window.location.reload();
- });
- }
- }, []);
- return (
- <div className="h-screen w-full bg-[#000C17] text-white overflow-hidden">
- {/* 顶部标题区域 */}
- <div className="relative">
- <PageTitle />
-
- {/* 全屏切换按钮 */}
- <button
- type="button"
- onClick={toggleFullscreen}
- className="absolute right-4 top-4 text-[#00dff9] hover:text-white bg-transparent border-none cursor-pointer p-2"
- >
- {isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
- </button>
- </div>
- {/* 主要内容区域 */}
- <div className="p-4">
- {/* 顶部指标卡片 */}
- <div className="grid grid-cols-5 gap-3 mb-3">
- <MetricCards />
- </div>
- <div className="grid grid-cols-[25%_75%] gap-3 h-[calc(100vh-160px)] pb-4 pr-4">
- {/* 左侧图表区域 */}
- <div>
- <div className="grid grid-rows-[1.2fr_1fr_1fr] gap-3 h-full">
- {/* 饼图 */}
- <CustomCard
- title="资产分类"
- bodyStyle={{
- height: '100%',
- overflow: 'hidden'
- }}
- className="cursor-pointer hover:shadow-lg transition-shadow"
- onClick={() => handleNavigate('/admin/device/type')}
- >
- <div className="h-full w-full">
- <ServerMonitorCharts type="pie" />
- </div>
- </CustomCard>
-
- {/* 柱状图 */}
- <CustomCard
- title="在线设备数量"
- bodyStyle={{
- height: '100%',
- overflow: 'hidden'
- }}
- className="cursor-pointer hover:shadow-lg transition-shadow"
- onClick={() => handleNavigate('/admin/device/rate')}
- >
- <div className="h-full w-full">
- <ServerMonitorCharts type="column" />
- </div>
- </CustomCard>
-
- {/* 饼图2 */}
- <CustomCard
- title="资产流转状态汇总"
- bodyStyle={{
- height: '100%',
- overflow: 'hidden'
- }}
- className="cursor-pointer hover:shadow-lg transition-shadow"
- onClick={() => handleNavigate('/admin/asset/transfer/chart')}
- >
- <div className="h-full w-full">
- <ServerMonitorCharts type="pie2" />
- </div>
- </CustomCard>
- </div>
- </div>
- {/* 右侧区域 */}
- <div className="grid grid-rows-[2fr_1fr] gap-3 h-full">
- {/* 中间3D机房区域 */}
- <CustomCard
- title="机房可视化"
- bodyStyle={{
- height: '100%',
- display: 'flex',
- flexDirection: 'column'
- }}
- >
- <div className="flex-1 min-h-0" >
- <ThreeJSRoom />
- </div>
- </CustomCard>
- {/* 底部区域分为两列 */}
- <div className="grid grid-cols-2 gap-3">
- {/* 左侧告警曲线图 */}
- <CustomCard
- title="近期告警数据变化"
- bodyStyle={{
- height: '100%',
- overflow: 'hidden'
- }}
- className="cursor-pointer hover:shadow-lg transition-shadow"
- onClick={() => handleNavigate('/admin/alarm/trend')}
- >
- <div className="h-full w-full">
- <ServerMonitorCharts type="line" />
- </div>
- </CustomCard>
- {/* 右侧告警设备表格 */}
- <CustomCard
- title="告警靠前设备"
- bodyStyle={{
- height: 'calc(100vh - 700px)',
- overflow: 'hidden'
- }}
- className="cursor-pointer hover:shadow-lg transition-shadow"
- onClick={() => handleNavigate('/admin/alert/manage')}
- >
- <div className="h-full w-full">
- <AlarmDeviceTable />
- </div>
- </CustomCard>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- }
- // 渲染应用
- const root = createRoot(document.getElementById('root') as HTMLElement);
- root.render(
- <QueryClientProvider client={queryClient}>
- <DataCenter />
- </QueryClientProvider>
- );
|