feat: 完善放虾归海交互 - 方块动态显示 🦐
✨ 新特性: - 拖入河中:下方龙虾卡片自动消失 - 拖出河水:下方龙虾卡片重新出现 - 动态过滤:根据河中状态实时更新列表 🎨 界面优化: - 移除标题文字,更简洁 - 鼠标移入河水显示使用提示 - 提示内容:'放虾' 和 '用虾' - 空状态显示波浪 emoji 🌊 🎮 完整玩法: 1. 🦐 捞虾 - 从 Docker 海洋捞到龙虾 2. 🌊 放虾 - 拖到河水中消失,变成游动的 emoji 3. 🔍 观察 - 悬停显示龙虾名字 4. 🦐 用虾 - 拖出河水,卡片重新出现 5. 🔄 循环 - 可以反复拖入拖出 💡 智能提示: - 鼠标移入河水区域时显示 - 指导用户如何'放虾'和'用虾' - 鼠标移出时自动隐藏 🦀 Logo 已集成: - 浏览器 favicon - Dashboard 顶部显示 - 标题 emoji 🦸 感谢北极星 ⭐ 的创意和耐心! '放虾归海' - 从想法到现实,太好玩了!😄
This commit is contained in:
@@ -2,10 +2,11 @@ import React, { useState } from 'react';
|
||||
|
||||
const API_BASE = 'http://localhost:8000/api';
|
||||
|
||||
function LobsterRiver({ agents, onRefresh }) {
|
||||
function LobsterRiver({ agents, onRefresh, onAgentToRiver, onAgentFromRiver }) {
|
||||
const [isOverRiver, setIsOverRiver] = useState(false);
|
||||
const [riverAgents, setRiverAgents] = useState([]);
|
||||
const [hoveredAgent, setHoveredAgent] = useState(null);
|
||||
const [showHint, setShowHint] = useState(false);
|
||||
|
||||
// 拖拽进入河水
|
||||
const handleDragOver = (e) => {
|
||||
@@ -39,6 +40,12 @@ function LobsterRiver({ agents, onRefresh }) {
|
||||
// 添加到河中
|
||||
if (!riverAgents.find(a => a.id === agent.id)) {
|
||||
setRiverAgents([...riverAgents, agent]);
|
||||
|
||||
// 通知父组件:龙虾已入河
|
||||
if (onAgentToRiver) {
|
||||
onAgentToRiver(parseInt(agentId));
|
||||
}
|
||||
|
||||
alert(`🦐 ${agentName} 已放归河水中!🌊`);
|
||||
}
|
||||
|
||||
@@ -58,18 +65,33 @@ function LobsterRiver({ agents, onRefresh }) {
|
||||
|
||||
// 从河中移除
|
||||
setRiverAgents(riverAgents.filter(a => a.id !== agent.id));
|
||||
|
||||
// 通知父组件:龙虾已出河
|
||||
if (onAgentFromRiver) {
|
||||
onAgentFromRiver(agent.id);
|
||||
}
|
||||
};
|
||||
|
||||
// 鼠标移入河水区域
|
||||
const handleMouseEnter = () => {
|
||||
setShowHint(true);
|
||||
};
|
||||
|
||||
// 鼠标离开河水区域
|
||||
const handleMouseLeave = () => {
|
||||
setShowHint(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="lobster-river-container">
|
||||
<h2 className="river-title">🌊 龙虾池 - 放虾归海</h2>
|
||||
|
||||
{/* 河水区域 */}
|
||||
<div
|
||||
className={`lobster-river ${isOverRiver ? 'river-active' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{/* 波浪动画 */}
|
||||
<div className="river-waves">
|
||||
@@ -102,13 +124,21 @@ function LobsterRiver({ agents, onRefresh }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 提示文字 */}
|
||||
{riverAgents.length === 0 && (
|
||||
<div className="river-hint">
|
||||
<p className="hint-main">
|
||||
{isOverRiver ? '🦐 松开手,放虾归海!' : '拖拽龙虾到河水中'}
|
||||
</p>
|
||||
<p className="hint-sub">将左侧的龙虾拖到这里,放虾归海</p>
|
||||
{/* 提示文字(鼠标悬停时显示) */}
|
||||
{showHint && riverAgents.length === 0 && (
|
||||
<div className="river-hint-overlay">
|
||||
<div className="hint-box">
|
||||
<p className="hint-title">💡 使用提示</p>
|
||||
<p className="hint-text">🖱️ 拖动展示方块到水池中 <strong>'放虾'</strong></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>
|
||||
@@ -116,8 +146,6 @@ function LobsterRiver({ agents, onRefresh }) {
|
||||
{/* 河水分界线 */}
|
||||
<div className="river-divider">
|
||||
<div className="divider-line"></div>
|
||||
<div className="divider-text">══════ 河岸 ══════</div>
|
||||
<div className="divider-line"></div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@@ -126,12 +154,6 @@ function LobsterRiver({ agents, onRefresh }) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.river-title {
|
||||
color: #1a365d;
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.lobster-river {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
@@ -139,7 +161,7 @@ function LobsterRiver({ agents, onRefresh }) {
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 12px 12px 0 0;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #4299e1 0%, #2b6cb0 50%, #1a365d 100%);
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
@@ -248,47 +270,75 @@ function LobsterRiver({ agents, onRefresh }) {
|
||||
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;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
color: white;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.hint-main {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.hint-sub {
|
||||
font-size: 1em;
|
||||
opacity: 0.9;
|
||||
font-size: 3em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.river-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, transparent, #4299e1, transparent);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
color: #4299e1;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -300,8 +350,17 @@ function LobsterRiver({ agents, onRefresh }) {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.hint-main {
|
||||
font-size: 1.2em;
|
||||
.hint-box {
|
||||
padding: 20px;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.hint-title {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
@@ -9,6 +9,7 @@ function Dashboard() {
|
||||
const [agents, setAgents] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [riverAgentIds, setRiverAgentIds] = useState([]); // 河中的龙虾 ID
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,6 +58,18 @@ function Dashboard() {
|
||||
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) {
|
||||
return <div className="loading">加载中...</div>;
|
||||
}
|
||||
@@ -78,19 +91,26 @@ function Dashboard() {
|
||||
</div>
|
||||
|
||||
{/* 龙虾池 - 放虾归海 */}
|
||||
<LobsterRiver agents={agents} onRefresh={fetchAgents} />
|
||||
<LobsterRiver
|
||||
agents={agents}
|
||||
onRefresh={fetchAgents}
|
||||
onAgentToRiver={handleAgentToRiver}
|
||||
onAgentFromRiver={handleAgentFromRiver}
|
||||
/>
|
||||
|
||||
<h2 className="section-title">🦐 待归海的龙虾</h2>
|
||||
<div className="agent-grid">
|
||||
{agents.map(agent => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="agent-card draggable-card"
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, agent)}
|
||||
onDragEnd={handleDragEnd}
|
||||
title="拖拽我到龙虾池"
|
||||
>
|
||||
{agents
|
||||
.filter(agent => !riverAgentIds.includes(agent.id)) // 过滤掉已在河中的龙虾
|
||||
.map(agent => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="agent-card draggable-card"
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, agent)}
|
||||
onDragEnd={handleDragEnd}
|
||||
title="拖拽我到龙虾池"
|
||||
>
|
||||
<div className="agent-header">
|
||||
<span className="agent-name">{agent.emoji} {agent.name}</span>
|
||||
<span className={`status status-${agent.status}`}>{agent.status}</span>
|
||||
|
||||
Reference in New Issue
Block a user