|
|
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;
|