You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

444 lines
16 KiB
JavaScript

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, { useMemo, useState } from 'react';
import {
Badge,
Button,
Card,
Col,
List,
Progress,
Row,
Space,
Statistic,
Tag,
Typography,
} from 'antd';
import {
CloudSyncOutlined,
DatabaseOutlined,
ExclamationCircleOutlined,
HddOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import styles from './Cjgl.less';
const { Text } = Typography;
const formatNumber = (value) => {
if (value === null || value === undefined) return '--';
const str = String(value);
return str.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
const buildConicGradient = (segments) => {
const total = segments.reduce((sum, s) => sum + (Number(s.value) || 0), 0) || 1;
let start = 0;
const stops = segments
.map((s) => {
const ratio = (Number(s.value) || 0) / total;
const end = start + ratio;
const result = `${s.color} ${Math.round(start * 360)}deg ${Math.round(end * 360)}deg`;
start = end;
return result;
})
.join(', ');
return `conic-gradient(${stops})`;
};
const Cjgl = () => {
const [activeAnomaly, setActiveAnomaly] = useState('missing');
const overviewMetrics = useMemo(
() => [
{
key: 'deviceTotal',
label: '采集设备',
value: 52,
suffix: '台',
tone: 'primary',
icon: <DatabaseOutlined />,
},
{
key: 'deviceOnline',
label: '在线设备',
value: 45,
suffix: '台',
tone: 'primary',
icon: <CloudSyncOutlined />,
},
{
key: 'todayCount',
label: '今日采集量',
value: 151235,
suffix: '',
tone: 'primary',
icon: <DatabaseOutlined />,
},
{
key: 'manualCount',
label: '手工录入',
value: 87,
suffix: '条',
tone: 'primary',
icon: <DatabaseOutlined />,
},
{
key: 'successRate',
label: '采集成功率',
value: 68,
suffix: '%',
tone: 'success',
icon: <CloudSyncOutlined />,
},
{
key: 'storageRate',
label: '存储使用率',
value: 89,
suffix: '%',
tone: 'warning',
icon: <HddOutlined />,
},
],
[],
);
const realtimeSummary = useMemo(
() => [
{ label: '运行中', value: 52, unit: '台', tone: 'primary' },
{ label: '设备类型', value: 5, unit: '种', tone: 'primary' },
{ label: '警告数', value: 3, unit: '条', tone: 'danger' },
],
[],
);
const devices = useMemo(
() =>
new Array(8).fill(0).map((_, idx) => ({
id: `SDO292938387-${idx + 1}`,
name: `变压器#${(idx % 3) + 1}`, // 仅作演示
frequency: '1分钟',
lastTime: '2024-06-10 14:29:30',
status: idx === 5 ? '异常' : '正常',
})),
[],
);
const anomalyTypes = useMemo(
() => [
{ key: 'missing', title: '数据缺失', desc: '范围超限', badge: 5 },
{ key: 'logic', title: '逻辑矛盾', desc: '', badge: 0 },
{ key: 'time', title: '时间异常', desc: '', badge: 0 },
],
[],
);
const anomalyCards = useMemo(
() =>
new Array(3).fill(0).map((_, idx) => ({
id: `anomaly-${activeAnomaly}-${idx}`,
deviceName: '变压器#1 (TR-001)',
missingTime: '14:20:00 至 14:25:00',
duration: '5分钟',
frequency: '10秒/次',
missingCount: '30条记录',
status: idx === 2 ? '待处理' : '已自动恢复',
})),
[activeAnomaly],
);
const sourceSegments = useMemo(
() => [
{ name: '自动采集', value: 23, color: '#3b82f6' },
{ name: '手工录入', value: 39, color: '#22c55e' },
{ name: '手工补录', value: 48, color: '#f59e0b' },
],
[],
);
const donutBg = useMemo(() => buildConicGradient(sourceSegments), [sourceSegments]);
const updateTime = useMemo(() => '2024-05-10 14:00:00', []);
return (
<div className={styles.container}>
<div className={styles.overview}>
<div className={styles.overviewHeader}>
<div className={styles.overviewTitle}>
<div className={styles.overviewIcon}>
<DatabaseOutlined />
</div>
<div className={styles.overviewText}>
<div className={styles.overviewMainTitle}>数据概览</div>
<div className={styles.overviewSubTitle}>采集与存储整体状态</div>
</div>
</div>
<div className={styles.overviewTime}>
<Text type="secondary">更新时间{updateTime}</Text>
</div>
</div>
<Row gutter={[12, 12]} className={styles.overviewCards}>
{overviewMetrics.map((m) => (
<Col key={m.key} xs={12} sm={8} md={8} lg={4} xl={4}>
<div className={`${styles.metricCard} ${styles[`metricCard_${m.tone}`]}`}>
<div className={styles.metricInner}>
<div className={styles.metricIcon}>{m.icon}</div>
<div className={styles.metricContent}>
<div className={styles.metricValue}>
{formatNumber(m.value)}
<span className={styles.metricSuffix}>{m.suffix}</span>
</div>
<div className={styles.metricLabel}>{m.label}</div>
</div>
</div>
</div>
</Col>
))}
</Row>
</div>
<Row gutter={[12, 12]} className={styles.middle}>
<Col xs={24} lg={8}>
<Card
className={styles.panelCard}
title={<span className={styles.panelTitle}>实时采集状态</span>}
extra={
<Button size="small" type="text" icon={<ReloadOutlined />} className={styles.iconBtn} />
}
>
<div className={styles.realtimeSummary}>
{realtimeSummary.map((s) => (
<div
key={s.label}
className={`${styles.summaryItem} ${styles[`summaryItem_${s.tone}`]}`}
>
<div className={styles.summaryValue}>
{s.value}
<span className={styles.summaryUnit}>{s.unit}</span>
</div>
<div className={styles.summaryLabel}>{s.label}</div>
</div>
))}
</div>
<div className={styles.deviceListWrap}>
<List
className={styles.deviceList}
dataSource={devices}
renderItem={(item) => (
<List.Item className={styles.deviceItem}>
<div className={styles.deviceRow}>
<div className={styles.deviceMain}>
<div className={styles.deviceTitle}>
<span className={styles.deviceName}>{item.name}</span>
<Tag className={styles.deviceId} color="cyan">
#{item.id}
</Tag>
</div>
<div className={styles.deviceMeta}>
<span>
<Text type="secondary">采集频率</Text>
{item.frequency}
</span>
<span>
<Text type="secondary">最后采集时间</Text>
{item.lastTime}
</span>
</div>
</div>
<div className={styles.deviceStatus}>
<Tag color={item.status === '正常' ? 'success' : 'error'}>
{item.status}
</Tag>
</div>
</div>
</List.Item>
)}
/>
</div>
</Card>
</Col>
<Col xs={24} lg={16}>
<Card
className={styles.panelCard}
title={<span className={styles.panelTitle}>今日数据异常统计</span>}
extra={
<Space size={8}>
<Button size="small">执行校验</Button>
<Button size="small">查看详情</Button>
<Button size="small">导出报告</Button>
</Space>
}
>
<div className={styles.anomalyBody}>
<div className={styles.anomalyTabs}>
{anomalyTypes.map((t) => (
<div
key={t.key}
className={`${styles.anomalyTab} ${
activeAnomaly === t.key ? styles.anomalyTabActive : ''
}`}
onClick={() => setActiveAnomaly(t.key)}
role="button"
tabIndex={0}
>
<div className={styles.anomalyTabTitleRow}>
<span className={styles.anomalyTabTitle}>{t.title}</span>
{t.badge ? (
<Badge count={t.badge} size="small" className={styles.anomalyBadge} />
) : null}
</div>
{t.desc ? <div className={styles.anomalyTabDesc}>{t.desc}</div> : null}
</div>
))}
</div>
<div className={styles.anomalyContent}>
<div className={styles.anomalyCards}>
{anomalyCards.map((c) => (
<div key={c.id} className={styles.anomalyCard}>
<div className={styles.anomalyCardHeader}>
<div className={styles.anomalyCardHeaderLeft}>
<ExclamationCircleOutlined className={styles.anomalyCardHeaderIcon} />
<span className={styles.anomalyCardHeaderTitle}>设备{c.deviceName}</span>
</div>
</div>
<div className={styles.anomalyCardBody}>
<div className={styles.anomalyField}>
<span className={styles.anomalyLabel}>缺失时间</span>
<span className={styles.anomalyValue}>{c.missingTime}</span>
</div>
<div className={styles.anomalyField}>
<span className={styles.anomalyLabel}>持续时间</span>
<span className={styles.anomalyValue}>{c.duration}</span>
</div>
<div className={styles.anomalyField}>
<span className={styles.anomalyLabel}>采集频率</span>
<span className={styles.anomalyValue}>{c.frequency}</span>
</div>
<div className={styles.anomalyField}>
<span className={styles.anomalyLabel}>缺失数据量</span>
<span className={styles.anomalyValue}>{c.missingCount}</span>
</div>
<div className={styles.anomalyField}>
<span className={styles.anomalyLabel}>处理状态</span>
<span className={styles.anomalyValue}>
<Tag color={c.status === '已自动恢复' ? 'success' : 'warning'}>
{c.status}
</Tag>
</span>
</div>
</div>
<div className={styles.anomalyCardFooter}>
<Button size="small" type="link">
查看详情
</Button>
</div>
</div>
))}
</div>
</div>
</div>
</Card>
</Col>
</Row>
<Row gutter={[12, 12]} className={styles.bottom}>
<Col xs={24} lg={14}>
<Card className={styles.panelCard} title={<span className={styles.panelTitle}>数据来源分布</span>}>
<div className={styles.donutSection}>
<div
className={styles.donut}
style={{
'--donut-bg': donutBg,
}}
>
<div className={styles.donutInner}>
<div className={styles.donutCenterTitle}>来源</div>
<div className={styles.donutCenterValue}>分布</div>
</div>
</div>
<div className={styles.donutLegends}>
{sourceSegments.map((s) => (
<div key={s.name} className={styles.legendItem}>
<span className={styles.legendDot} style={{ background: s.color }} />
<span className={styles.legendName}>{s.name}</span>
<span className={styles.legendValue}>{s.value}%</span>
</div>
))}
</div>
</div>
</Card>
</Col>
<Col xs={24} lg={10}>
<Card
className={styles.panelCard}
title={<span className={styles.panelTitle}>存储空间使用预测</span>}
extra={<Text type="secondary">已用总量</Text>}
>
<div className={styles.storageSection}>
<div className={styles.storageLegend}>
<div className={styles.storageLegendItem}>
<span className={styles.legendDot} style={{ background: '#8b5cf6' }} />
<span className={styles.legendName}>已用总量</span>
<span className={styles.legendValue}>54TB</span>
</div>
<div className={styles.storageLegendItem}>
<span className={styles.legendDot} style={{ background: '#60a5fa' }} />
<span className={styles.legendName}>近30天预测</span>
<span className={styles.legendValue}>54TB</span>
</div>
<div className={styles.storageLegendItem}>
<span className={styles.legendDot} style={{ background: '#4ade80' }} />
<span className={styles.legendName}>近60天预测</span>
<span className={styles.legendValue}>54TB</span>
</div>
</div>
<div className={styles.storageRings}>
<div className={styles.storageRingLayer}>
<Progress
type="circle"
percent={82}
size={200}
strokeWidth={10}
strokeColor="#8b5cf6"
trailColor="#f1f5f9"
format={() => '54TB'}
/>
</div>
<div className={`${styles.storageRingLayer} ${styles.storageRingLayer_mid}`}>
<Progress
type="circle"
percent={70}
size={160}
strokeWidth={10}
strokeColor="#60a5fa"
trailColor="transparent"
format={() => ''}
/>
</div>
<div className={`${styles.storageRingLayer} ${styles.storageRingLayer_inner}`}>
<Progress
type="circle"
percent={55}
size={120}
strokeWidth={10}
strokeColor="#4ade80"
trailColor="transparent"
format={() => ''}
/>
</div>
</div>
</div>
</Card>
</Col>
</Row>
</div>
);
};
export default Cjgl;