feat: 完善放虾归海交互 - 方块动态显示 🦐

 新特性:
- 拖入河中:下方龙虾卡片自动消失
- 拖出河水:下方龙虾卡片重新出现
- 动态过滤:根据河中状态实时更新列表

🎨 界面优化:
- 移除标题文字,更简洁
- 鼠标移入河水显示使用提示
- 提示内容:'放虾' 和 '用虾'
- 空状态显示波浪 emoji 🌊

🎮 完整玩法:
1. 🦐 捞虾 - 从 Docker 海洋捞到龙虾
2. 🌊 放虾 - 拖到河水中消失,变成游动的 emoji
3. 🔍 观察 - 悬停显示龙虾名字
4. 🦐 用虾 - 拖出河水,卡片重新出现
5. 🔄 循环 - 可以反复拖入拖出

💡 智能提示:
- 鼠标移入河水区域时显示
- 指导用户如何'放虾'和'用虾'
- 鼠标移出时自动隐藏

🦀 Logo 已集成:
- 浏览器 favicon
- Dashboard 顶部显示
- 标题 emoji

🦸 感谢北极星  的创意和耐心!
'放虾归海' - 从想法到现实,太好玩了!😄
This commit is contained in:
2026-04-03 21:14:36 +08:00
parent b48964ebf6
commit ac5dd3a91e
2 changed files with 137 additions and 58 deletions

View File

@@ -2,10 +2,11 @@ import React, { useState } from 'react';
const API_BASE = 'http://localhost:8000/api'; const API_BASE = 'http://localhost:8000/api';
function LobsterRiver({ agents, onRefresh }) { function LobsterRiver({ agents, onRefresh, onAgentToRiver, onAgentFromRiver }) {
const [isOverRiver, setIsOverRiver] = useState(false); const [isOverRiver, setIsOverRiver] = useState(false);
const [riverAgents, setRiverAgents] = useState([]); const [riverAgents, setRiverAgents] = useState([]);
const [hoveredAgent, setHoveredAgent] = useState(null); const [hoveredAgent, setHoveredAgent] = useState(null);
const [showHint, setShowHint] = useState(false);
// 拖拽进入河水 // 拖拽进入河水
const handleDragOver = (e) => { const handleDragOver = (e) => {
@@ -39,6 +40,12 @@ function LobsterRiver({ agents, onRefresh }) {
// 添加到河中 // 添加到河中
if (!riverAgents.find(a => a.id === agent.id)) { if (!riverAgents.find(a => a.id === agent.id)) {
setRiverAgents([...riverAgents, agent]); setRiverAgents([...riverAgents, agent]);
// 通知父组件:龙虾已入河
if (onAgentToRiver) {
onAgentToRiver(parseInt(agentId));
}
alert(`🦐 ${agentName} 已放归河水中!🌊`); alert(`🦐 ${agentName} 已放归河水中!🌊`);
} }
@@ -58,18 +65,33 @@ function LobsterRiver({ agents, onRefresh }) {
// 从河中移除 // 从河中移除
setRiverAgents(riverAgents.filter(a => a.id !== agent.id)); setRiverAgents(riverAgents.filter(a => a.id !== agent.id));
// 通知父组件:龙虾已出河
if (onAgentFromRiver) {
onAgentFromRiver(agent.id);
}
};
// 鼠标移入河水区域
const handleMouseEnter = () => {
setShowHint(true);
};
// 鼠标离开河水区域
const handleMouseLeave = () => {
setShowHint(false);
}; };
return ( return (
<div className="lobster-river-container"> <div className="lobster-river-container">
<h2 className="river-title">🌊 龙虾池 - 放虾归海</h2>
{/* 河水区域 */} {/* 河水区域 */}
<div <div
className={`lobster-river ${isOverRiver ? 'river-active' : ''}`} className={`lobster-river ${isOverRiver ? 'river-active' : ''}`}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
> >
{/* 波浪动画 */} {/* 波浪动画 */}
<div className="river-waves"> <div className="river-waves">
@@ -102,13 +124,21 @@ function LobsterRiver({ agents, onRefresh }) {
))} ))}
</div> </div>
{/* 提示文字 */} {/* 提示文字(鼠标悬停时显示) */}
{riverAgents.length === 0 && ( {showHint && riverAgents.length === 0 && (
<div className="river-hint"> <div className="river-hint-overlay">
<p className="hint-main"> <div className="hint-box">
{isOverRiver ? '🦐 松开手,放虾归海!' : '拖拽龙虾到河水中'} <p className="hint-title">💡 使用提示</p>
</p> <p className="hint-text">🖱 拖动展示方块到水池中 <strong>'放虾'</strong></p>
<p className="hint-sub">将左侧的龙虾拖到这里放虾归海</p> <p className="hint-text">🦐 拖动虾到池子 <strong>'用虾'</strong></p>
</div>
</div>
)}
{/* 空状态提示 */}
{!showHint && riverAgents.length === 0 && (
<div className="river-empty-state">
<span className="empty-emoji">🌊</span>
</div> </div>
)} )}
</div> </div>
@@ -116,8 +146,6 @@ function LobsterRiver({ agents, onRefresh }) {
{/* 河水分界线 */} {/* 河水分界线 */}
<div className="river-divider"> <div className="river-divider">
<div className="divider-line"></div> <div className="divider-line"></div>
<div className="divider-text"> 河岸 </div>
<div className="divider-line"></div>
</div> </div>
<style>{` <style>{`
@@ -126,12 +154,6 @@ function LobsterRiver({ agents, onRefresh }) {
text-align: center; text-align: center;
} }
.river-title {
color: #1a365d;
font-size: 1.5em;
margin-bottom: 15px;
}
.lobster-river { .lobster-river {
width: 100%; width: 100%;
max-width: 1000px; max-width: 1000px;
@@ -139,7 +161,7 @@ function LobsterRiver({ agents, onRefresh }) {
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
border-radius: 12px 12px 0 0; border-radius: 12px;
background: linear-gradient(180deg, #4299e1 0%, #2b6cb0 50%, #1a365d 100%); background: linear-gradient(180deg, #4299e1 0%, #2b6cb0 50%, #1a365d 100%);
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
@@ -248,47 +270,75 @@ function LobsterRiver({ agents, onRefresh }) {
z-index: 30; z-index: 30;
} }
.river-hint { .river-hint-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 20;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.hint-box {
background: rgba(255, 255, 255, 0.95);
padding: 30px 40px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.hint-title {
color: #1a365d;
font-size: 1.3em;
font-weight: bold;
margin-bottom: 15px;
}
.hint-text {
color: #4a5568;
font-size: 1em;
margin: 10px 0;
line-height: 1.6;
}
.hint-text strong {
color: #4299e1;
font-weight: 600;
}
.river-empty-state {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 10; z-index: 10;
color: white; font-size: 3em;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); opacity: 0.5;
}
.hint-main {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 10px;
}
.hint-sub {
font-size: 1em;
opacity: 0.9;
} }
.river-divider { .river-divider {
display: flex; width: 100%;
align-items: center;
justify-content: center;
gap: 20px;
margin: 0 auto;
max-width: 1000px; max-width: 1000px;
margin: 0 auto;
} }
.divider-line { .divider-line {
flex: 1;
height: 3px; height: 3px;
background: linear-gradient(90deg, transparent, #4299e1, transparent); background: linear-gradient(90deg, transparent, #4299e1, transparent);
} margin-top: -3px;
.divider-text {
color: #4299e1;
font-size: 1.2em;
font-weight: bold;
letter-spacing: 2px;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -300,8 +350,17 @@ function LobsterRiver({ agents, onRefresh }) {
font-size: 1.5em; font-size: 1.5em;
} }
.hint-main { .hint-box {
font-size: 1.2em; padding: 20px;
margin: 0 20px;
}
.hint-title {
font-size: 1.1em;
}
.hint-text {
font-size: 0.9em;
} }
} }
`}</style> `}</style>

View File

@@ -9,6 +9,7 @@ function Dashboard() {
const [agents, setAgents] = useState([]); const [agents, setAgents] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [scanning, setScanning] = useState(false); const [scanning, setScanning] = useState(false);
const [riverAgentIds, setRiverAgentIds] = useState([]); // 河中的龙虾 ID
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
@@ -57,6 +58,18 @@ function Dashboard() {
e.target.style.opacity = '1'; e.target.style.opacity = '1';
}; };
// 龙虾放入河中
const handleAgentToRiver = (agentId) => {
if (!riverAgentIds.includes(agentId)) {
setRiverAgentIds([...riverAgentIds, agentId]);
}
};
// 龙虾从河中拖出
const handleAgentFromRiver = (agentId) => {
setRiverAgentIds(riverAgentIds.filter(id => id !== agentId));
};
if (loading) { if (loading) {
return <div className="loading">加载中...</div>; return <div className="loading">加载中...</div>;
} }
@@ -78,11 +91,18 @@ function Dashboard() {
</div> </div>
{/* 龙虾池 - 放虾归海 */} {/* 龙虾池 - 放虾归海 */}
<LobsterRiver agents={agents} onRefresh={fetchAgents} /> <LobsterRiver
agents={agents}
onRefresh={fetchAgents}
onAgentToRiver={handleAgentToRiver}
onAgentFromRiver={handleAgentFromRiver}
/>
<h2 className="section-title">🦐 待归海的龙虾</h2> <h2 className="section-title">🦐 待归海的龙虾</h2>
<div className="agent-grid"> <div className="agent-grid">
{agents.map(agent => ( {agents
.filter(agent => !riverAgentIds.includes(agent.id)) // 过滤掉已在河中的龙虾
.map(agent => (
<div <div
key={agent.id} key={agent.id}
className="agent-card draggable-card" className="agent-card draggable-card"