2026-04-01 20:43:25 +08:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
2026-04-02 13:15:41 +08:00
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
2026-04-01 20:43:25 +08:00
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
|
|
|
|
|
|
const API_BASE = 'http://localhost:8000/api';
|
|
|
|
|
|
|
|
|
|
|
|
function Dashboard() {
|
|
|
|
|
|
const [lobsters, setLobsters] = useState([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2026-04-02 13:15:41 +08:00
|
|
|
|
const navigate = useNavigate();
|
2026-04-01 20:43:25 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchLobsters();
|
|
|
|
|
|
const interval = setInterval(fetchLobsters, 5000);
|
|
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const fetchLobsters = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await axios.get(`${API_BASE}/lobsters/`);
|
|
|
|
|
|
setLobsters(response.data);
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取龙虾状态失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return <div className="loading">加载中...</div>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="dashboard">
|
|
|
|
|
|
<h1>🦞 龙虾舰队监控中心</h1>
|
|
|
|
|
|
<div className="lobster-grid">
|
|
|
|
|
|
{lobsters.map(lobster => (
|
|
|
|
|
|
<div key={lobster.id} className="lobster-card">
|
|
|
|
|
|
<div className="lobster-header">
|
|
|
|
|
|
<span className="lobster-name">{lobster.emoji} {lobster.name}</span>
|
|
|
|
|
|
<span className={`status status-${lobster.status}`}>{lobster.status}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="lobster-info">
|
|
|
|
|
|
<p>专长:{lobster.specialty}</p>
|
|
|
|
|
|
<p>端口:{lobster.port}</p>
|
|
|
|
|
|
<p>容器:{lobster.container}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="lobster-actions">
|
2026-04-02 13:15:41 +08:00
|
|
|
|
<button className="detail-btn" onClick={() => navigate(`/lobster/${lobster.id}`)}>
|
2026-04-01 20:43:25 +08:00
|
|
|
|
📊 详情
|
|
|
|
|
|
</button>
|
2026-04-02 13:15:41 +08:00
|
|
|
|
<button className="memory-btn" onClick={() => navigate(`/lobster/${lobster.id}/memory`)}>
|
2026-04-01 20:43:25 +08:00
|
|
|
|
🧠 记忆
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default Dashboard;
|
2026-04-02 13:15:41 +08:00
|
|
|
|
|
|
|
|
|
|
// CSS 样式
|
|
|
|
|
|
const styles = `
|
|
|
|
|
|
.dashboard {
|
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dashboard h1 {
|
|
|
|
|
|
color: #1a365d;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lobster-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lobster-card {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
transition: transform 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lobster-card:hover {
|
|
|
|
|
|
transform: translateY(-4px);
|
|
|
|
|
|
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lobster-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
|
border-bottom: 2px solid #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lobster-name {
|
|
|
|
|
|
font-size: 1.3em;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #2d3748;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status {
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
font-size: 0.85em;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-healthy {
|
|
|
|
|
|
background: #c6f6d5;
|
|
|
|
|
|
color: #22543d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-warning {
|
|
|
|
|
|
background: #feebc8;
|
|
|
|
|
|
color: #744210;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-error {
|
|
|
|
|
|
background: #fed7d7;
|
|
|
|
|
|
color: #742a2a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lobster-info {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lobster-info p {
|
|
|
|
|
|
margin: 8px 0;
|
|
|
|
|
|
color: #4a5568;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lobster-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lobster-actions button {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-size: 1em;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-btn {
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-btn:hover {
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.memory-btn {
|
|
|
|
|
|
background: #edf2f7;
|
|
|
|
|
|
color: #4a5568;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.memory-btn:hover {
|
|
|
|
|
|
background: #e2e8f0;
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 50px;
|
|
|
|
|
|
color: #718096;
|
|
|
|
|
|
font-size: 1.2em;
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 注入样式
|
|
|
|
|
|
if (typeof document !== 'undefined') {
|
|
|
|
|
|
const styleSheet = document.createElement('style');
|
|
|
|
|
|
styleSheet.textContent = styles;
|
|
|
|
|
|
document.head.appendChild(styleSheet);
|
|
|
|
|
|
}
|