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

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;