装备控制界面
This commit is contained in:
53
package-lock.json
generated
53
package-lock.json
generated
@@ -26,6 +26,7 @@
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.14.1",
|
||||
"recharts": "^3.8.1",
|
||||
@@ -4598,6 +4599,18 @@
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -4828,6 +4841,15 @@
|
||||
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@@ -4991,6 +5013,23 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -5702,6 +5741,20 @@
|
||||
"react": "^19.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/react-draggable": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
|
||||
"integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0",
|
||||
"react-dom": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.14.1",
|
||||
"recharts": "^3.8.1",
|
||||
|
||||
14
src/api/home.tsx
Normal file
14
src/api/home.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import request from './request';
|
||||
|
||||
//新增物模型
|
||||
export function getWeatherData(data) {
|
||||
return request({
|
||||
url: '/system/devicemodel/add',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
86
src/components/DraggableModal.jsx
Normal file
86
src/components/DraggableModal.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
|
||||
const DraggableModal = ({
|
||||
open,
|
||||
onCancel,
|
||||
title,
|
||||
width = 420,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const startPos = useRef({ x: 0, y: 0 });
|
||||
|
||||
// 打开时重置位置
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setPosition({ x: 0, y: 0 });
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// 拖拽开始
|
||||
const handleMouseDown = (e) => {
|
||||
startPos.current = {
|
||||
x: e.clientX - position.x,
|
||||
y: e.clientY - position.y,
|
||||
};
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
// 拖拽中
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isDragging) return;
|
||||
setPosition({
|
||||
x: e.clientX - startPos.current.x,
|
||||
y: e.clientY - startPos.current.y,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isDragging]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={width}
|
||||
mask={false}
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
transition: isDragging ? 'none' : 'transform 0.2s ease',
|
||||
}}
|
||||
title={
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraggableModal;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card, Row, Col, Select, DatePicker, Input, Button, Space, Table, Tag,
|
||||
Statistic, Typography
|
||||
Statistic, Typography, Modal, Form, Checkbox, message
|
||||
} from 'antd';
|
||||
import {
|
||||
BellOutlined, ClockCircleOutlined, CheckCircleOutlined, ExclamationCircleOutlined
|
||||
@@ -34,9 +34,20 @@ const mockAlerts = [
|
||||
{ id: 'AL-20260428-003', content: 'CAM-02 视频离线', device: 'CAM-02', level: '提示', time: '2026-04-28 09:32:45', duration: '0分钟', status: '待处理', handler: '-' },
|
||||
];
|
||||
|
||||
// 通知渠道
|
||||
const channelOptions = [
|
||||
{ label: '企业微信', value: 'wecom' },
|
||||
{ label: '钉钉', value: 'dingtalk' },
|
||||
{ label: '邮件', value: 'email' },
|
||||
{ label: '短信', value: 'sms' },
|
||||
];
|
||||
|
||||
export default function AlarmCenterPage() {
|
||||
const [selectedAlert, setSelectedAlert] = useState<any>(mockAlerts[0]);
|
||||
const [detailVisible, setDetailVisible] = useState(true);
|
||||
const [confirmModalVisible, setConfirmModalVisible] = useState(false);
|
||||
const [notifyChannels, setNotifyChannels] = useState<string[]>(['wecom', 'dingtalk', 'email']);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const columns = [
|
||||
{ title: '告警编号', dataIndex: 'id', width: 180 },
|
||||
@@ -131,19 +142,20 @@ export default function AlarmCenterPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 提交确认告警
|
||||
const handleConfirmAlarm = () => {
|
||||
form.validateFields().then(values => {
|
||||
console.log('确认告警:', selectedAlert.id, values.notifyChannels);
|
||||
message.success('告警已确认,通知将发送至:' + values.notifyChannels.join(','));
|
||||
setConfirmModalVisible(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ background: '#f5f7fa', minHeight: '100vh', padding: '0 24px 24px' }}>
|
||||
|
||||
{/* ===================== 统一宽度容器 ===================== */}
|
||||
<div style={{ display: 'flex', gap: 16, marginTop: 16, width: '100%' }}>
|
||||
|
||||
{/* ===================== 左侧:所有模块宽度完全一致 ===================== */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
|
||||
{/* 统一容器:保证 筛选 + 统计 宽度完全一样 */}
|
||||
<div style={{ width: "100%" }}>
|
||||
|
||||
{/* 筛选卡片 */}
|
||||
<Card style={{ borderRadius: 8, border: 'none', marginBottom: 16 }} bodyStyle={{ padding: '16px' }}>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col><Select placeholder="告警级别" style={{ width: 120 }} /></Col>
|
||||
@@ -156,144 +168,46 @@ export default function AlarmCenterPage() {
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 统计卡片 - 现在和筛选卡片一样宽 */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "16px",
|
||||
marginBottom: 16,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Card bordered={false} style={{ borderRadius: 8, padding: '12px' }} bodyStyle={{ padding: '12px 0 0 0' }}>
|
||||
<Statistic
|
||||
title="今日告警总数"
|
||||
value={5}
|
||||
prefix={<BellOutlined style={{ color: '#ff4d4f' }} />}
|
||||
valueStyle={{ fontSize: 28, fontWeight: 700, color: '#ff4d4f' }}
|
||||
/>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>昨日 28</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Card bordered={false} style={{ borderRadius: 8, padding: '12px' }} bodyStyle={{ padding: '8px 0 0 0' }}>
|
||||
<Statistic
|
||||
title="严重告警"
|
||||
value={0}
|
||||
prefix={<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />}
|
||||
valueStyle={{ fontSize: 28, fontWeight: 700, color: '#ff4d4f' }}
|
||||
/>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>重要 2</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Card bordered={false} style={{ borderRadius: 8, padding: '12px' }} bodyStyle={{ padding: '8px 0 0 0' }}>
|
||||
<Statistic
|
||||
title="处理中"
|
||||
value={1}
|
||||
prefix={<ClockCircleOutlined style={{ color: '#faad14' }} />}
|
||||
valueStyle={{ fontSize: 28, fontWeight: 700, color: '#faad14' }}
|
||||
/>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>待处理 4</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Card bordered={false} style={{ borderRadius: 8, padding: '12px' }} bodyStyle={{ padding: '8px 0 0 0' }}>
|
||||
<Statistic
|
||||
title="已关闭"
|
||||
value={0}
|
||||
prefix={<CheckCircleOutlined style={{ color: '#52c41a' }} />}
|
||||
valueStyle={{ fontSize: 28, fontWeight: 700, color: '#52c41a' }}
|
||||
/>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>闭环完成</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Card bordered={false} style={{ borderRadius: 8, padding: '12px' }} bodyStyle={{ padding: '8px 0 0 0' }}>
|
||||
<Statistic
|
||||
title="平均响应"
|
||||
value="49分钟"
|
||||
prefix={<ClockCircleOutlined style={{ color: '#1890ff' }} />}
|
||||
valueStyle={{ fontSize: 28, fontWeight: 700, color: '#1890ff' }}
|
||||
/>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>时间估算</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "16px", marginBottom: 16, width: "100%" }}>
|
||||
<div style={{ flex: 1 }}><Card bordered={false} style={{ borderRadius: 8, padding: '12px' }} bodyStyle={{ padding: '12px 0 0 0' }}>
|
||||
<Statistic title="今日告警总数" value={5} prefix={<BellOutlined style={{ color: '#ff4d4f' }} />} valueStyle={{ fontSize: 28, fontWeight: 700, color: '#ff4d4f' }} />
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>昨日 28</div>
|
||||
</Card></div>
|
||||
<div style={{ flex: 1 }}><Card bordered={false} style={{ borderRadius: 8, padding: '12px' }} bodyStyle={{ padding: '8px 0 0 0' }}>
|
||||
<Statistic title="严重告警" value={0} prefix={<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />} valueStyle={{ fontSize: 28, fontWeight: 700, color: '#ff4d4f' }} />
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>重要 2</div>
|
||||
</Card></div>
|
||||
<div style={{ flex: 1 }}><Card bordered={false} style={{ borderRadius: 8, padding: '12px' }} bodyStyle={{ padding: '8px 0 0 0' }}>
|
||||
<Statistic title="处理中" value={1} prefix={<ClockCircleOutlined style={{ color: '#faad14' }} />} valueStyle={{ fontSize: 28, fontWeight: 700, color: '#faad14' }} />
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>待处理 4</div>
|
||||
</Card></div>
|
||||
<div style={{ flex: 1 }}><Card bordered={false} style={{ borderRadius: 8, padding: '12px' }} bodyStyle={{ padding: '8px 0 0 0' }}>
|
||||
<Statistic title="已关闭" value={0} prefix={<CheckCircleOutlined style={{ color: '#52c41a' }} />} valueStyle={{ fontSize: 28, fontWeight: 700, color: '#52c41a' }} />
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>闭环完成</div>
|
||||
</Card></div>
|
||||
<div style={{ flex: 1 }}><Card bordered={false} style={{ borderRadius: 8, padding: '12px' }} bodyStyle={{ padding: '8px 0 0 0' }}>
|
||||
<Statistic title="平均响应" value="49分钟" prefix={<ClockCircleOutlined style={{ color: '#1890ff' }} />} valueStyle={{ fontSize: 28, fontWeight: 700, color: '#1890ff' }} />
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>时间估算</div>
|
||||
</Card></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 告警列表(宽度 = 筛选卡片 完全一致) */}
|
||||
{/* 告警列表 + 右侧详情(不换行、表格自适应、可横向滚动) */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 16,
|
||||
width: '100%',
|
||||
flexWrap: 'nowrap', /* 强制不换行 */
|
||||
alignItems: 'stretch', /* 高度对齐 */
|
||||
marginBottom: 16
|
||||
}}>
|
||||
|
||||
{/* 左侧告警列表:自适应宽度 + 横向滚动 */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
minWidth: 0, /* 关键:允许表格缩小 */
|
||||
}}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 16, width: '100%', flexWrap: 'nowrap', alignItems: 'stretch', marginBottom: 16 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Card bordered={false} style={{ borderRadius: 8, marginBottom: 16, height: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<div>
|
||||
<Typography.Text strong style={{ fontSize: 16, color: '#0837A8', fontWeight: 800 }}>告警列表(共8条)</Typography.Text><br />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>点击告警查看详情</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={mockAlerts}
|
||||
pagination={{ pageSize: 8 }}
|
||||
scroll={{ x: 'max-content' }} /* 表格超出自动横向滚动 */
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Table rowKey="id" columns={columns} dataSource={mockAlerts} pagination={{ pageSize: 8 }} scroll={{ x: 'max-content' }} style={{ width: '100%' }} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧详情:固定 360px,不挤压、不换行 */}
|
||||
{detailVisible && selectedAlert && (
|
||||
<div style={{
|
||||
width: 360,
|
||||
flexShrink: 0, /* 关键:不被挤压 */
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<Card
|
||||
title="告警详情"
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
bodyStyle={{
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
|
||||
{/* 可滚动内容区域 */}
|
||||
<div style={{ width: 360, flexShrink: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<Card title="告警详情" bordered={false} style={{ borderRadius: 8, height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} bodyStyle={{ padding: '16px', display: 'flex', flexDirection: 'column', flex: 1, overflow: 'hidden' }}>
|
||||
<div style={{ flex: 1, overflow: 'auto', paddingRight: 8 }}>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
@@ -325,10 +239,9 @@ export default function AlarmCenterPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 固定底部操作按钮 */}
|
||||
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid #f0f0f0' }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Button type="primary" block>确认告警</Button>
|
||||
<Button type="primary" block onClick={() => setConfirmModalVisible(true)}>确认告警</Button>
|
||||
<Button block>标记处理中</Button>
|
||||
<Button block danger>关闭告警</Button>
|
||||
</Space>
|
||||
@@ -338,46 +251,43 @@ export default function AlarmCenterPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 三个图表卡片:告警趋势 + 分类占比 + 设备分布 */}
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ borderRadius: 8 }}
|
||||
title="告警趋势"
|
||||
extra={<Button type="link">更多 </Button>}
|
||||
>
|
||||
<Card bordered={false} style={{ borderRadius: 8 }} title="告警趋势" extra={<Button type="link">更多 </Button>}>
|
||||
<Line {...trendConfig} height={180} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ borderRadius: 8 }}
|
||||
title="告警分类占比"
|
||||
extra={<Button type="link">更多 </Button>}
|
||||
>
|
||||
<Card bordered={false} style={{ borderRadius: 8 }} title="告警分类占比" extra={<Button type="link">更多 </Button>}>
|
||||
<Pie {...pieConfig} height={180} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ borderRadius: 8 }}
|
||||
title="告警来源设备分布"
|
||||
extra={<Button type="link">更多</Button>}
|
||||
>
|
||||
<Card bordered={false} style={{ borderRadius: 8 }} title="告警来源设备分布" extra={<Button type="link">更多</Button>}>
|
||||
<Bar {...barConfig} height={180} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ===================== 右侧详情面板:高度拉伸至视口 ===================== */}
|
||||
|
||||
</div>
|
||||
|
||||
</div >
|
||||
{/* 确认告警弹窗 */}
|
||||
<Modal
|
||||
title="确认告警"
|
||||
open={confirmModalVisible}
|
||||
onCancel={() => setConfirmModalVisible(false)}
|
||||
onOk={handleConfirmAlarm}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} initialValues={{ notifyChannels }}>
|
||||
<Form.Item label="发送通知至" name="notifyChannels">
|
||||
<Checkbox.Group options={channelOptions} />
|
||||
</Form.Item>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>
|
||||
确认后将向选中渠道发送通知
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,3 +48,17 @@
|
||||
opacity: 1;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
|
||||
@keyframes sunRotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes cloudFloat {
|
||||
from { transform: translateX(-2px); }
|
||||
to { transform: translateX(2px); }
|
||||
}
|
||||
@keyframes rainDrop {
|
||||
0% { transform: translateY(0); opacity: 1; }
|
||||
100% { transform: translateY(12px); opacity: 0.4; }
|
||||
}
|
||||
|
||||
@@ -61,3 +61,37 @@ export const buildPermTree = (treeData) => {
|
||||
};
|
||||
return loop(treeData);
|
||||
};
|
||||
|
||||
|
||||
export async function getWeatherData(options = {}) {
|
||||
// 默认参数配置
|
||||
const defaultPayload = {
|
||||
city: "苏州",
|
||||
adcode: "",
|
||||
extended: false,
|
||||
forecast: false,
|
||||
hourly: false,
|
||||
minutely: false,
|
||||
indices: false,
|
||||
lang: "zh"
|
||||
};
|
||||
|
||||
// 合并用户传入参数与默认参数
|
||||
const payload = { ...defaultPayload, ...options };
|
||||
|
||||
try {
|
||||
// 动态导入 UapiClient SDK
|
||||
// @ts-ignore
|
||||
const { UapiClient } = await import('https://cdn.jsdelivr.net/npm/uapi-browser-sdk@latest/dist/index.js');
|
||||
|
||||
// 初始化客户端(注意:需替换为真实有效的 API_KEY)
|
||||
const client = new UapiClient('https://uapis.cn', 'uapi-qy9ybtxlQ3Pp0JsHl7gXTAX45v0QpIeOoRbOnymq');
|
||||
|
||||
// 调用天气接口并返回数据
|
||||
const weatherData = await client.misc.getMiscWeather(payload);
|
||||
return weatherData;
|
||||
} catch (error) {
|
||||
console.error('[WeatherAPI] 获取天气数据失败:', error);
|
||||
throw new Error(`天气接口调用失败:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,289 +1,426 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Tag,
|
||||
Progress,
|
||||
Space,
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Descriptions,
|
||||
Tabs,
|
||||
Card
|
||||
Layout, Card, Row, Col, Statistic, Tag, Button, Table,
|
||||
Typography, Space, Progress, Badge, Timeline, message
|
||||
} from 'antd';
|
||||
import {
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
SettingOutlined,
|
||||
PoweroffOutlined,
|
||||
ClusterOutlined,
|
||||
ArrowRightOutlined,
|
||||
CloseOutlined,
|
||||
DashboardOutlined,
|
||||
HeatMapOutlined,
|
||||
InfoCircleOutlined,
|
||||
LineChartOutlined,
|
||||
ThunderboltOutlined
|
||||
EnvironmentOutlined, RobotOutlined, BranchesOutlined,
|
||||
ThunderboltOutlined, CheckCircleOutlined, PauseOutlined,
|
||||
RocketOutlined, SyncOutlined, WarningOutlined,
|
||||
CloudOutlined, ClockCircleOutlined, ReloadOutlined,
|
||||
AimOutlined, ApiOutlined, SettingOutlined,
|
||||
PlusOutlined, ArrowRightOutlined,
|
||||
EditOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { DEVICES } from '../constants';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
|
||||
const { Content } = Layout;
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export default function Devices() {
|
||||
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
|
||||
const selectedDevice = DEVICES.find(d => d.id === selectedDeviceId);
|
||||
// ------------------- 模拟数据 -------------------
|
||||
const topCards = [
|
||||
{ title: '无人机机场状态', icon: <EnvironmentOutlined />, value: '2 / 2', desc: '在线', sub: '可调度 2 架', color: '#1890ff' },
|
||||
{ title: '巡检机器人', icon: <RobotOutlined />, value: '3 台', desc: '', sub: '执行中 2', color: '#722ed1' },
|
||||
{ title: '除草机器人', icon: <BranchesOutlined />, value: '2 台', desc: '', sub: '作业中 1', color: '#13c2c2' },
|
||||
{ title: '清洗无人机', icon: <CloudOutlined />, value: '2 架', desc: '', sub: '待命 1', color: '#eb2f96' },
|
||||
{ title: '设备平均电量', icon: <ThunderboltOutlined />, value: '86%', desc: '', sub: '', color: '#52c41a' },
|
||||
{ title: '今日任务执行', icon: <CheckCircleOutlined />, value: '12 / 15', desc: '', sub: '完成率 80%', color: '#2f54eb' },
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '节点身份',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text: string, record: any) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong style={{ fontSize: 13 }}>{text}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 10, fontFamily: 'monospace' }}>{record.id} | {record.type}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '负载/状态',
|
||||
key: 'status',
|
||||
render: (record: any) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text
|
||||
strong
|
||||
italic
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: record.status === 'online' ? '#52c41a' : record.status === 'alert' ? '#f5222d' : '#bfbfbf'
|
||||
}}
|
||||
>
|
||||
{record.status.toUpperCase()}
|
||||
</Text>
|
||||
<Space size="small">
|
||||
<ClusterOutlined style={{ fontSize: 10, color: 'rgba(0,0,0,0.25)' }} />
|
||||
<Text type="secondary" style={{ fontSize: 11, fontFamily: 'monospace' }}>{record.cpu}%</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '能量效率',
|
||||
dataIndex: 'battery',
|
||||
key: 'battery',
|
||||
render: (percent: number) => (
|
||||
<Space size="middle">
|
||||
<Progress
|
||||
percent={percent}
|
||||
size={[60, 4]}
|
||||
showInfo={false}
|
||||
status={percent < 20 ? 'exception' : 'active'}
|
||||
strokeColor={percent < 20 ? '#f5222d' : percent < 50 ? '#faad14' : '#52c41a'}
|
||||
/>
|
||||
<Text style={{ fontSize: 11, fontFamily: 'monospace' }}>{percent}%</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'battery',
|
||||
key: 'battery',
|
||||
render: (percent: number) => (
|
||||
<Space size="middle">
|
||||
<Progress
|
||||
percent={percent}
|
||||
size={[60, 4]}
|
||||
showInfo={false}
|
||||
status={percent < 20 ? 'exception' : 'active'}
|
||||
strokeColor={percent < 20 ? '#f5222d' : percent < 50 ? '#faad14' : '#52c41a'}
|
||||
/>
|
||||
<Text style={{ fontSize: 11, fontFamily: 'monospace' }}>{percent}%</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '管理',
|
||||
key: 'action',
|
||||
align: 'right' as const,
|
||||
render: () => (
|
||||
<Space>
|
||||
<Button type="text" size="small" icon={<SettingOutlined />} />
|
||||
<Button type="text" size="small" danger icon={<PoweroffOutlined />} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
const scheduleList = [
|
||||
{ id: 1, name: '无人机机场 A01', type: '无人机机场', status: '在线', mode: '待命', battery: 100, position: '开压站旁', task: '待命', comm: '良好' },
|
||||
{ id: 2, name: '巡检机器人 RBT-01', type: '巡检机器人', status: '作业中', mode: '自主巡检', battery: 68, position: '区块 B-03', task: '巡检航线 P-02', comm: '良好' },
|
||||
{ id: 3, name: '巡检机器人 RBT-02', type: '巡检机器人', status: '在线', mode: '待命', battery: 92, position: '区块 D-06', task: '待命', comm: '良好' },
|
||||
{ id: 4, name: '除草机器人 GR-01', type: '除草机器人', status: '充电中', mode: '回充', battery: 35, position: '充电桩 1', task: '回充中', comm: '良好' },
|
||||
{ id: 5, name: '清洗无人机 UAV-C01', type: '清洗无人机', status: '作业中', mode: '自主清洗', battery: 72, position: '清洗区 C-05', task: '清洗航线 C-05', comm: '良好' },
|
||||
{ id: 6, name: '清洗无人机 UAV-C02', type: '清洗无人机', status: '待命', mode: '待命', battery: 96, position: '无人机机场 A01', task: '待命', comm: '良好' },
|
||||
];
|
||||
|
||||
const taskTimeline = [
|
||||
{ time: '10:24:18', color: 'blue', text: 'UAV-C01 开始执行清洗航线 C-05', status: '任务开始' },
|
||||
{ time: '10:21:35', color: 'orange', text: 'RBT-01 开始执行巡检航线 P-02', status: '任务开始' },
|
||||
{ time: '10:18:02', color: 'green', text: 'GR-02 开始除草作业 G-01', status: '任务开始' },
|
||||
{ time: '10:15:47', color: 'gray', text: 'UAV-C02 任务待命就绪', status: '待命就绪' },
|
||||
{ time: '10:12:33', color: 'blue', text: 'UAV-C01 再次返机补水', status: '返机补给' },
|
||||
{ time: '09:58:10', color: 'green', text: 'RBT-02 完成巡检航线 P-01', status: '任务完成' },
|
||||
];
|
||||
|
||||
const logs = [
|
||||
{ time: '10:24:35', device: 'UAV-C01', type: '命令', desc: '起飞指令下发,开始作业' },
|
||||
{ time: '10:21:40', device: 'RBT-01', type: '命令', desc: '开始巡检任务 P-02' },
|
||||
{ time: '10:20:11', device: 'UAV-C02', type: '命令', desc: '转入待命状态' },
|
||||
{ time: '10:18:05', device: 'GR-02', type: '命令', desc: '除草作业开始 G-01' },
|
||||
{ time: '10:12:35', device: 'UAV-C01', type: '告警', desc: '返航补水' },
|
||||
{ time: '10:05:22', device: 'RBT-01', type: '事件', desc: '电量较低,建议回充', level: '低电量' },
|
||||
];
|
||||
|
||||
// ------------------- 主组件 -------------------
|
||||
export default function EquipmentCooperationPage() {
|
||||
const [activeTab, setActiveTab] = useState('drone');
|
||||
|
||||
const controlTabs = [
|
||||
{ key: 'airport', label: '无人机机场' },
|
||||
{ key: 'robot', label: '巡检机器人' },
|
||||
{ key: 'mower', label: '除草机器人' },
|
||||
{ key: 'clean', label: '清洗无人机' },
|
||||
{ key: 'machine', label: '机械设备' },
|
||||
];
|
||||
|
||||
const handleStart = () => message.success('任务已启动');
|
||||
const handlePause = () => message.info('任务已暂停');
|
||||
const handleReturn = () => message.info('返航指令已下发');
|
||||
const handleRecharge = () => message.info('回充指令已下发');
|
||||
const handleEmergencyStop = () => message.error('已触发急停!');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title level={5} style={{ margin: 0 }}>核心资产管理系统</Title>
|
||||
<Space>
|
||||
<Input
|
||||
prefix={<SearchOutlined style={{ color: 'rgba(0,0,0,0.25)' }} />}
|
||||
placeholder="搜索节点、ID或类型..."
|
||||
style={{ width: 250, borderRadius: 2 }}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} style={{ borderRadius: 2 }}>
|
||||
接入新节点
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Layout style={{ background: '#f5f7fa', minHeight: '100vh', padding: '20px' }}>
|
||||
<Content style={{ maxWidth: 1800, margin: '0 auto' }}>
|
||||
{/* 顶部统计卡片 */}
|
||||
{/* 顶部统计卡片 - 正方形 + 自动换行 */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', marginBottom: 20 }}>
|
||||
{topCards.map((card, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
flex: 1,
|
||||
|
||||
<Row gutter={16} className="flex-1 " style={{ minHeight: 0, flexWrap: 'nowrap', }}>
|
||||
<Col flex="auto" className="h-full overflow-hidden">
|
||||
<Card styles={{ body: { padding: 0 } }} className="h-full overflow-hidden flex flex-col">
|
||||
<Table
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={DEVICES.map(d => ({ ...d, key: d.id }))}
|
||||
pagination={false}
|
||||
sticky
|
||||
scroll={{ y: 'calc(100vh - 250px)' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => setSelectedDeviceId(record.id),
|
||||
className: selectedDeviceId === record.id ? 'bg-blue-50' : '',
|
||||
style: { cursor: 'pointer' }
|
||||
})}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<AnimatePresence>
|
||||
{selectedDevice && (
|
||||
<Col flex="0 0 340px" className="h-full">
|
||||
<motion.div
|
||||
initial={{ x: 340, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 340, opacity: 0 }}
|
||||
className="h-full"
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.05)',
|
||||
height: '120px', // 固定高度
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
className="h-full overflow-hidden"
|
||||
styles={{ body: { padding: 0, height: '100%', display: 'flex', flexDirection: 'column' } }}
|
||||
>
|
||||
<div className="p-4 border-b border-gray-100 flex justify-between items-start bg-gray-50">
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 10, letterSpacing: 2, textTransform: 'uppercase', display: 'block' }}>
|
||||
Asset Intelligence / v4.0
|
||||
</Text>
|
||||
<Title level={4} style={{ margin: '4px 0' }}>{selectedDevice.name}</Title>
|
||||
<Space>
|
||||
<Tag color="blue" variant={false} style={{ fontSize: 10, fontWeight: 'bold' }}>{selectedDevice.type}</Tag>
|
||||
<Text type="secondary" style={{ fontSize: 11, fontFamily: 'monospace' }}>ID: {selectedDevice.id}</Text>
|
||||
</Space>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 10,
|
||||
background: `${card.color}15`,
|
||||
color: card.color,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 20,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{card.icon}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 13, color: '#666' }}>{card.title}</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 700, color: '#222' }}>{card.value}</div>
|
||||
{card.sub && <div style={{ fontSize: 12, color: '#999' }}>{card.sub}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Row gutter={16}>
|
||||
{/* 左侧:装备协同态势图 */}
|
||||
<Col xs={24} lg={14}>
|
||||
<Card
|
||||
title={<><EnvironmentOutlined /> 装备协同态势图</>}
|
||||
extra={<Button type="text" icon={<SettingOutlined />} />}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 12, marginBottom: 16 }}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div style={{
|
||||
width: '100%', height: 400,
|
||||
background: '#e6f7ff url(https://picsum.photos/seed/solarfarm/1600/800) center/cover',
|
||||
borderRadius: 12,
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* 图例 */}
|
||||
<div style={{ position: 'absolute', top: 16, left: 16, background: 'rgba(255,255,255,0.95)', borderRadius: 8, padding: 12 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 8 }}>图例说明</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 11 }}>
|
||||
<div><span style={{ color: '#1890ff' }}>———</span> 巡检航线</div>
|
||||
<div><span style={{ color: '#fa8c16' }}>———</span> 机器人路径</div>
|
||||
<div><span style={{ color: '#52c41a' }}>———</span> 清洗路径</div>
|
||||
<div><span style={{ color: '#52c41a' }}>●</span> 设备位置</div>
|
||||
<div><span style={{ color: '#1890ff' }}>●</span> 补给/充电点</div>
|
||||
<div><span style={{ color: '#ff4d4f' }}>■</span> 障碍/禁入区</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 设备标注(示意) */}
|
||||
<div style={{ position: 'absolute', top: 80, left: 120, background: 'rgba(255,255,255,0.9)', borderRadius: 8, padding: 8, fontSize: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Badge status="success" /> 无人机机场 A01
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#666' }}>正常</div>
|
||||
</div>
|
||||
<div style={{ position: 'absolute', top: 180, left: 280, background: 'rgba(255,255,255,0.9)', borderRadius: 8, padding: 8, fontSize: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Badge status="processing" /> 巡检机器人 RBT-01
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#666' }}>作业中</div>
|
||||
</div>
|
||||
<div style={{ position: 'absolute', top: 80, right: 120, background: 'rgba(255,255,255,0.9)', borderRadius: 8, padding: 8, fontSize: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Badge status="processing" /> 清洗无人机 UAV-C01
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#666' }}>作业中</div>
|
||||
</div>
|
||||
<div style={{ position: 'absolute', bottom: 120, right: 180, background: 'rgba(255,255,255,0.9)', borderRadius: 8, padding: 8, fontSize: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Badge status="processing" /> 除草机器人 GR-02
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#666' }}>作业中</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 装备调度列表 */}
|
||||
<Card title={<><SettingOutlined /> 装备调度列表</>} bordered={false} style={{ borderRadius: 12, marginBottom: 16 }}>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '装备名称', dataIndex: 'name', width: 160 },
|
||||
{ title: '类型', dataIndex: 'type', width: 100 },
|
||||
{ title: '当前状态', dataIndex: 'status', width: 80, render: (s) => <Tag color={s === '作业中' ? 'processing' : s === '在线' ? 'success' : 'default'}>{s}</Tag> },
|
||||
{ title: '模式', dataIndex: 'mode', width: 80 },
|
||||
{ title: '电量', dataIndex: 'battery', width: 80, render: (v) => <Progress percent={v} size="small" status={v < 40 ? 'exception' : 'active'} /> },
|
||||
{ title: '位置', dataIndex: 'position', width: 100 },
|
||||
{ title: '当前任务', dataIndex: 'task', width: 120 },
|
||||
{ title: '通讯状态', dataIndex: 'comm', width: 80, render: (v) => <Badge status="success" text={v} /> },
|
||||
{ title: '操作', key: 'action', width: 100, render: () => <Space><Button size="small" type="text" icon={<EditOutlined />} /><Button size="small" type="text" icon={<SettingOutlined />} /></Space> }
|
||||
]}
|
||||
dataSource={scheduleList}
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 任务执行时间线 + 环境信息 */}
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Card title={<><ClockCircleOutlined /> 任务执行时间线</>} bordered={false} style={{ borderRadius: 12 }} extra={<div style={{ fontSize: 12, color: '#666' }}>今天 2025-05-24</div>}>
|
||||
<Timeline mode="left">
|
||||
{taskTimeline.map((item, idx) => (
|
||||
<Timeline.Item key={idx} color={item.color} label={<div style={{ fontSize: 11, color: '#999' }}>{item.time}</div>}>
|
||||
<div style={{ fontSize: 13 }}>{item.text}</div>
|
||||
<div style={{ fontSize: 11, color: '#999' }}>{item.status}</div>
|
||||
</Timeline.Item>
|
||||
))}
|
||||
</Timeline>
|
||||
<div style={{ textAlign: 'center', marginTop: 12 }}><Button type="link">查看更多</Button></div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Card title={<><CloudOutlined /> 环境与飞行条件</>} bordered={false} style={{ borderRadius: 12 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}><div style={{ fontSize: 12, color: '#999' }}>风速</div><div style={{ fontSize: 14 }}>2.6 m/s 轻风</div></Col>
|
||||
<Col span={12}><div style={{ fontSize: 12, color: '#999' }}>温度</div><div style={{ fontSize: 14 }}>28.7℃ 适宜</div></Col>
|
||||
<Col span={12}><div style={{ fontSize: 12, color: '#999' }}>辐照度</div><div style={{ fontSize: 14 }}>812 W/m² 较高</div></Col>
|
||||
<Col span={12}><div style={{ fontSize: 12, color: '#999' }}>降雨概率</div><div style={{ fontSize: 14 }}>10% 低</div></Col>
|
||||
<Col span={12}><div style={{ fontSize: 12, color: '#999' }}>GNSS状态</div><div style={{ fontSize: 14 }}>正常 定位稳定</div></Col>
|
||||
<Col span={12}>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>禁飞状态</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<div style={{ fontSize: 14 }}>未禁飞</div>
|
||||
<Badge status="success" />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{ marginTop: 16, background: '#f6ffed', borderRadius: 8, padding: 10, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 500 }}>适宜执行</div>
|
||||
<div style={{ fontSize: 11, color: '#666' }}>可正常开展作业</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => setSelectedDeviceId(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
className="px-4"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: '基本信息',
|
||||
children: (
|
||||
<div className="space-y-6 pt-4 pb-4">
|
||||
<Descriptions column={1} size="small" variant>
|
||||
<Descriptions.Item label="设备名称">{selectedDevice.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="设备 ID">{selectedDevice.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="设备类型">{selectedDevice.type}</Descriptions.Item>
|
||||
<Descriptions.Item label="最后心跳">{selectedDevice.lastUpdate}</Descriptions.Item>
|
||||
<Descriptions.Item label="固件版本">FW_2.5.4_REL</Descriptions.Item>
|
||||
<Descriptions.Item label="接入协议">MQTT_SSL</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<div className="p-3 bg-blue-50 border border-blue-100 rounded">
|
||||
<Space size="small" align="start">
|
||||
<InfoCircleOutlined style={{ color: '#1890ff', marginTop: 3 }} />
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
该设备目前运行正常,已通过节点身份验证,正在进行全量数据透传。
|
||||
</Text>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '运行状态',
|
||||
children: (
|
||||
<div className="space-y-6 pt-4 pb-4">
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={12}>
|
||||
<Card size="small" variant styles={{ body: { padding: 12 } }} style={{ backgroundColor: '#fafafa' }}>
|
||||
<Statistic title={<Space><ClusterOutlined />负载率</Space>} value={selectedDevice.cpu} suffix="%" valueStyle={{ fontSize: 16, fontFamily: 'monospace' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card size="small" variant styles={{ body: { padding: 12 } }} style={{ backgroundColor: '#fafafa' }}>
|
||||
<Statistic title={<Space><HeatMapOutlined />核心温度</Space>} value={selectedDevice.temp} suffix="°C" valueStyle={{ fontSize: 16, fontFamily: 'monospace', color: '#f5222d' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card size="small" variant styles={{ body: { padding: 12 } }} style={{ backgroundColor: '#fafafa' }}>
|
||||
<Statistic title={<Space><ThunderboltOutlined />剩余能量</Space>} value={selectedDevice.battery} suffix="%" valueStyle={{ fontSize: 16, fontFamily: 'monospace', color: '#52c41a' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card size="small" variant styles={{ body: { padding: 12 } }} style={{ backgroundColor: '#fafafa' }}>
|
||||
<Statistic title={<Space><DashboardOutlined />系统健康</Space>} value={98.2} suffix="%" valueStyle={{ fontSize: 16, fontFamily: 'monospace' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="pt-2 border-t border-gray-100">
|
||||
<Text strong type="secondary" style={{ fontSize: 11, textTransform: 'uppercase', display: 'block', marginBottom: 12 }}>
|
||||
实时动态 (SIGNAL)
|
||||
</Text>
|
||||
<Space direction="vertical" className="w-full" size="small">
|
||||
<div className="flex justify-between items-center bg-gray-50 p-2 rounded">
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>上行链路速率</Text>
|
||||
<Text style={{ fontFamily: 'monospace', fontSize: 11 }}>1.2 Mbps</Text>
|
||||
</div>
|
||||
<div className="flex justify-between items-center bg-gray-50 p-2 rounded">
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>下行链路速率</Text>
|
||||
<Text style={{ fontFamily: 'monospace', fontSize: 11 }}>0.4 Mbps</Text>
|
||||
</div>
|
||||
<div className="flex justify-between items-center bg-gray-50 p-2 rounded">
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>当前信号强度</Text>
|
||||
<Tag color="success" style={{ margin: 0, fontSize: 10 }}>EXCELLENT</Tag>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50">
|
||||
<Space direction="vertical" className="w-full" size="small">
|
||||
<Button type="primary" block icon={<ArrowRightOutlined />} style={{ borderRadius: 2 }}>
|
||||
发起远程诊断
|
||||
</Button>
|
||||
<Button block icon={<LineChartOutlined />} style={{ borderRadius: 2 }}>
|
||||
同步设备策略
|
||||
</Button>
|
||||
</Space>
|
||||
<Button type="primary" size="small">一键作业</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Col>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Row>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
{/* 右侧:设备控制面板 */}
|
||||
<Col xs={24} lg={10}>
|
||||
<Card title={<><EnvironmentOutlined /> 设备控制面板</>} bordered={false} style={{ borderRadius: 12, marginBottom: 16 }} bodyStyle={{ padding: 16 }}>
|
||||
{/* 标签栏 */}
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 20, flexWrap: 'wrap' }}>
|
||||
{controlTabs.map(tab => (
|
||||
<Button
|
||||
key={tab.key}
|
||||
size="large"
|
||||
type={activeTab === tab.key ? 'primary' : 'text'}
|
||||
style={{
|
||||
padding: "0 12px",
|
||||
height: "36px",
|
||||
borderRadius: 8,
|
||||
border: activeTab === tab.key ? '1px solid #1890ff' : '1px solid #d9d9d9',
|
||||
color: activeTab === tab.key ? '#1890ff' : '#000',
|
||||
fontSize: "14px",
|
||||
background: 'transparent', // 无背景
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: activeTab === tab.key ? '#1890ff' : '#d9d9d9',
|
||||
}}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 主体:设备信息 + 实时画面 双栏 */}
|
||||
<Row gutter={16}>
|
||||
{/* 左侧:设备信息 */}
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
{/* 设备标题行 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
||||
<span style={{ fontSize: 24, color: '#1890ff' }}>
|
||||
<RocketOutlined />
|
||||
</span>
|
||||
<span style={{ fontSize: 16, fontWeight: 600 }}>清洗无人机 UAV-C01</span>
|
||||
<Tag color="success" size="small">作业中</Tag>
|
||||
</div>
|
||||
|
||||
{/* 电量/信号行 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ color: '#52c41a' }}><ThunderboltOutlined /></span>
|
||||
<span style={{ fontSize: 12 }}>电量 72%</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ fontSize: 12 }}>信号强度</span>
|
||||
<span style={{ color: '#52c41a' }}>强</span>
|
||||
<span style={{ fontSize: 16, color: '#1890ff' }}></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 信息列表 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666', fontSize: 12 }}>当前模式</span>
|
||||
<span style={{ fontSize: 12, }}>自主清洗模式</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666', fontSize: 12 }}>当前位置</span>
|
||||
<span style={{ fontSize: 12, }}>清洗区域 C-05</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666', fontSize: 12 }}>当前任务</span>
|
||||
<span style={{ fontSize: 12, }}>清洗航线 C-05</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666', fontSize: 12 }}>预计剩余时间</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>00:18:36</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 控制按钮 */}
|
||||
<div style={{ display: 'flex', justifyContent: "space-between", flexWrap: 'wrap' }}>
|
||||
<Button type="primary" size="large" style={{ borderRadius: 8, padding: "0 10px", fontSize: "14px" }} onClick={handleStart}>启动任务</Button>
|
||||
<Button style={{ borderRadius: 8, padding: "0 10px", fontSize: "14px" }} size="large" onClick={handlePause}>暂停</Button>
|
||||
<Button style={{ borderRadius: 8, padding: "0 10px", fontSize: "14px" }} type="primary" size="large" onClick={handleReturn}>返航</Button>
|
||||
<Button style={{ background: '#52c41a', padding: "0 10px", fontSize: "14px", borderColor: '#52c41a', borderRadius: 8, color: "#fff" }} size="large" onClick={handleRecharge}>回充</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{/* 右侧:实时画面 */}
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: "#666" }}>实时画面</span>
|
||||
<span style={{ fontSize: 13, color: '#999' }}>2025-05-24 10:24:18</span>
|
||||
</div>
|
||||
<div style={{
|
||||
width: '100%', height: 180,
|
||||
background: '#000 url(https://picsum.photos/seed/droneview/800/400) center/cover',
|
||||
borderRadius: 8,
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* 底部参数条 */}
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, background: 'rgba(0,0,0,0.6)', color: '#fff', display: 'flex', justifyContent: 'space-between', padding: '8px 12px', fontSize: 14 }}>
|
||||
<span>高度 12.5m</span>
|
||||
<span>速度 4.2m/s</span>
|
||||
<span>距离 86m</span>
|
||||
<span style={{ color: '#ffd666' }}></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 下方按钮 */}
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
<Button size="large" style={{ flex: 1, borderRadius: 8, padding: "0 10px", fontSize: "14px" }}>切换手动</Button>
|
||||
<Button
|
||||
danger
|
||||
size="large"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 8,
|
||||
padding: "0 10px",
|
||||
fontSize: "14px",
|
||||
backgroundColor: "#ff4d4f", // 纯红色背景
|
||||
borderColor: "#ff4d4f",
|
||||
color: "#fff"
|
||||
}}
|
||||
onClick={handleEmergencyStop}
|
||||
>
|
||||
急停
|
||||
</Button> </div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* AI分析与操作建议 */}
|
||||
<Card title={<><ApiOutlined /> AI分析与操作建议</>} bordered={false} style={{ borderRadius: 12, marginBottom: 16 }}>
|
||||
<Row gutter={16} style={{ marginBottom: 12 }}>
|
||||
<Col span={6}><div style={{ display: 'flex', alignItems: 'center', gap: 4 }}><WarningOutlined style={{ color: '#ff4d4f' }} /><div><div style={{ fontSize: 11, color: '#999' }}>热斑疑似</div><div style={{ fontSize: 14, fontWeight: 600 }}>2 处</div></div></div></Col>
|
||||
<Col span={6}><div style={{ display: 'flex', alignItems: 'center', gap: 4 }}><BranchesOutlined style={{ color: '#52c41a' }} /><div><div style={{ fontSize: 11, color: '#999' }}>杂草高发区</div><div style={{ fontSize: 14, fontWeight: 600 }}>3 块</div></div></div></Col>
|
||||
<Col span={6}><div style={{ display: 'flex', alignItems: 'center', gap: 4 }}><CloudOutlined style={{ color: '#1890ff' }} /><div><div style={{ fontSize: 11, color: '#999' }}>组件污损风险</div><div style={{ fontSize: 14, fontWeight: 600 }}>4 块</div></div></div></Col>
|
||||
<Col span={6}><div style={{ display: 'flex', alignItems: 'center', gap: 4 }}><AimOutlined style={{ color: '#faad14' }} /><div><div style={{ fontSize: 11, color: '#999' }}>围栏异常</div><div style={{ fontSize: 14, fontWeight: 600 }}>1 处</div></div></div></Col>
|
||||
</Row>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}><ArrowRightOutlined style={{ fontSize: 10 }} /> 优先执行巡检航线 P-03</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}><ArrowRightOutlined style={{ fontSize: 10 }} /> 先行安排除草任务 G-02</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}><ArrowRightOutlined style={{ fontSize: 10 }} /> 建议生成清洗工单</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button type="primary" size="small">生成任务</Button>
|
||||
<Button size="small">AI优化路径</Button>
|
||||
<Button type="primary" size="small">一键联动作业</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 控制日志/告警记录 */}
|
||||
<Card title={<><SettingOutlined /> 控制日志 / 告警记录</>} bordered={false} style={{ borderRadius: 12 }} extra={<Button type="link" size="small">查看更多日志</Button>}>
|
||||
<div style={{ maxHeight: 300, overflow: 'auto' }}>
|
||||
{logs.map((log, idx) => (
|
||||
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<div style={{ color: '#999', marginBottom: 2 }}>{log.time}</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Tag size="small" color={log.type === '命令' ? 'blue' : log.type === '告警' ? 'orange' : 'default'}>{log.type}</Tag>
|
||||
<div>{log.device} - {log.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
{log.level && <Tag color="red" size="small">{log.level}</Tag>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Row, Col, Statistic, Button, Space, Tag, Typography } from 'antd';
|
||||
import {
|
||||
CloudOutlined, ThunderboltOutlined, BarChartOutlined,
|
||||
@@ -6,10 +6,43 @@ import {
|
||||
MinusOutlined, PlusOutlined, SyncOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { Line, Bar, Pie } from '@ant-design/charts';
|
||||
import { getWeatherData } from '../lib/utils';
|
||||
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
||||
export default function StationDashboard() {
|
||||
// ==============================================
|
||||
// 🔥 接入真实天气状态(从你第一个组件复制过来)
|
||||
// ==============================================
|
||||
const [weatherdata, setWeatherData] = useState({
|
||||
province: "江苏省",
|
||||
city: "南通市",
|
||||
district: "崇川区",
|
||||
adcode: "320613",
|
||||
weather: "晴",
|
||||
weather_icon: "100",
|
||||
temperature: 18,
|
||||
wind_direction: "西南风",
|
||||
wind_power: "2级",
|
||||
humidity: 53,
|
||||
report_time: "2026-03-27 12:35:08"
|
||||
});
|
||||
|
||||
// 请求天气接口
|
||||
useEffect(() => {
|
||||
const fetchWeather = async () => {
|
||||
try {
|
||||
const res = await getWeatherData();
|
||||
setWeatherData(res);
|
||||
} catch (err) {
|
||||
console.error("天气接口请求失败", err);
|
||||
}
|
||||
};
|
||||
fetchWeather();
|
||||
}, []);
|
||||
|
||||
// 模拟数据
|
||||
const stationOverview = {
|
||||
todayEnergy: 45.9,
|
||||
@@ -471,7 +504,6 @@ export default function StationDashboard() {
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 天气信息 */}
|
||||
<Card
|
||||
title={<span style={{ fontWeight: 800, fontSize: 14, color: '#0837A8' }}>天气信息</span>}
|
||||
bordered={false}
|
||||
@@ -482,22 +514,25 @@ export default function StationDashboard() {
|
||||
}}
|
||||
bodyStyle={{ padding: 4 }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CloudOutlined style={{ fontSize: 32, color: '#1890ff', marginRight: 12 }} />
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: 12, alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', position: 'relative', }}>
|
||||
|
||||
|
||||
|
||||
{/* 右侧文字 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: 24, alignItems: 'center', width: "100%" }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 24, fontWeight: 800, color: '#1890ff' }}>22℃</div>
|
||||
<div style={{ fontSize: 14, color: '#666', marginBottom: 2 }}>阴天</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 800, color: '#1890ff' }}>{weatherdata.temperature}℃</div>
|
||||
<div style={{ fontSize: 14, color: '#666' }}>{weatherdata.weather}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#999' }}>辐照度 89 W/m²</div>
|
||||
<div style={{ fontSize: 11, color: '#999' }}>风速 4.1 m/s</div>
|
||||
<div style={{ fontSize: 11, color: '#999' }}>中卫市</div>
|
||||
<div style={{ fontSize: 11, color: '#999' }}>湿度 {weatherdata.humidity}%</div>
|
||||
<div style={{ fontSize: 11, color: '#999' }}>{weatherdata.wind_direction} {weatherdata.wind_power}</div>
|
||||
<div style={{ fontSize: 11, color: '#999' }}>{weatherdata.city} | {weatherdata.district}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 清洗建议 */}
|
||||
<Card
|
||||
title={
|
||||
|
||||
Reference in New Issue
Block a user