装备控制界面

This commit is contained in:
mmc
2026-05-09 16:44:39 +08:00
parent a7aa27242f
commit 48c6d1d4bc
9 changed files with 725 additions and 441 deletions

53
package-lock.json generated
View File

@@ -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",

View File

@@ -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
View File

@@ -0,0 +1,14 @@
import request from './request';
//新增物模型
export function getWeatherData(data) {
return request({
url: '/system/devicemodel/add',
method: 'post',
data
});
}

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

View File

@@ -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>
);
}

View File

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

View File

@@ -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}`);
}
}

View File

@@ -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>
);
}

View File

@@ -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={