feat: 添加放虾归海功能(龙虾河)🦐🌊

🎨 新设计:
- 龙虾河:200px 高,蓝色渐变背景
- 波浪动画:三层半透明波浪滚动效果
- 河水分界线:贯穿的蓝线 + '河岸'文字
- 河中的龙虾:随机位置,游动动画

🦐 交互玩法:
1. 拖拽龙虾卡片到河水中
2. 龙虾变成 emoji 在河中游动
3. 鼠标悬停显示名字 tooltip
4. 可以从河中拖回下方列表

 功能特性:
- 龙虾在河中随机分布(10%-90% 位置)
- 游动动画(上下浮动 + 左右摇摆)
- 悬停显示名字
- 拖拽出河水后从河中移除
- 成功提示:'XX 已放归河水中!🌊'

🦀 Logo 更新:
- 添加 favicon(浏览器标签页图标)
- Dashboard 顶部显示 logo
- 标题 emoji 更新为 🦀

🎯 组件重构:
- LobsterPool → LobsterRiver(更贴切)
- 简化组件结构
- 优化动画效果

🦸 感谢北极星  的创意!
'放虾归海' - 太好玩了!😄
This commit is contained in:
2026-04-03 21:02:38 +08:00
parent 52ef5cc095
commit b48964ebf6
5 changed files with 418 additions and 33 deletions

View File

@@ -3,7 +3,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🤖 Agent Diary - AI Agent 日记管理系统</title> <title>🦀 Agent Diary - AI Agent 日记管理系统</title>
<link rel="icon" href="/logo.png" type="image/png">
<link rel="shortcut icon" href="/logo.png" type="image/png">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

View File

@@ -5,11 +5,13 @@ const API_BASE = 'http://localhost:8000/api';
function LobsterPool({ agents, onRefresh }) { function LobsterPool({ agents, onRefresh }) {
const [isDragging, setIsDragging] = useState(null); const [isDragging, setIsDragging] = useState(null);
const [isOverPool, setIsOverPool] = useState(false); const [isOverPool, setIsOverPool] = useState(false);
const [droppedAgents, setDroppedAgents] = useState([]);
// 开始拖拽 // 开始拖拽
const handleDragStart = (e, agent) => { const handleDragStart = (e, agent) => {
setIsDragging(agent); setIsDragging(agent);
e.dataTransfer.setData('agentId', agent.id); e.dataTransfer.setData('agentId', agent.id.toString());
e.dataTransfer.setData('agentName', agent.name);
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
}; };
@@ -36,30 +38,20 @@ function LobsterPool({ agents, onRefresh }) {
setIsOverPool(false); setIsOverPool(false);
const agentId = e.dataTransfer.getData('agentId'); const agentId = e.dataTransfer.getData('agentId');
const agentName = e.dataTransfer.getData('agentName');
if (agentId) {
// 添加到已放入列表
const agent = agents.find(a => a.id === parseInt(agentId)); const agent = agents.find(a => a.id === parseInt(agentId));
if (agent && !droppedAgents.find(a => a.id === agent.id)) {
if (agent) { setDroppedAgents([...droppedAgents, agent]);
try {
// 调用 API 同步到数据库
const response = await fetch(`${API_BASE}/agents/sync/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
agent_id: agent.id,
action: 'add_to_pool',
}),
});
const data = await response.json();
if (data.success) {
alert(`🦐 ${agent.name} 已放虾归海!`);
onRefresh(); // 刷新列表
} }
} catch (error) {
console.error('放虾失败:', error); alert(`🦐 ${agentName} 已放虾归海!🌊`);
alert('❌ 放虾失败:' + error.message);
// 刷新列表(可选:这里可以调用 API 真正同步到数据库)
if (onRefresh) {
setTimeout(() => onRefresh(), 500);
} }
} }
}; };
@@ -88,6 +80,20 @@ function LobsterPool({ agents, onRefresh }) {
{isOverPool ? '🦐 松开手,放虾归海!' : '拖拽龙虾到池中'} {isOverPool ? '🦐 松开手,放虾归海!' : '拖拽龙虾到池中'}
</p> </p>
<p className="pool-hint">将左侧的龙虾拖到这里自动同步到数据库</p> <p className="pool-hint">将左侧的龙虾拖到这里自动同步到数据库</p>
{/* 已放入的龙虾 */}
{droppedAgents.length > 0 && (
<div className="dropped-agents">
<p className="dropped-title">已归海的龙虾 ({droppedAgents.length})</p>
<div className="dropped-list">
{droppedAgents.map(agent => (
<span key={agent.id} className="dropped-agent">
{agent.emoji} {agent.name}
</span>
))}
</div>
</div>
)}
</div> </div>
</div> </div>
@@ -114,6 +120,7 @@ function LobsterPool({ agents, onRefresh }) {
border: 3px solid #4299e1; border: 3px solid #4299e1;
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
background: linear-gradient(180deg, #4299e1 0%, #2b6cb0 100%);
} }
.lobster-pool.pool-active { .lobster-pool.pool-active {
@@ -128,8 +135,8 @@ function LobsterPool({ agents, onRefresh }) {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: linear-gradient(180deg, #4299e1 0%, #2b6cb0 100%);
overflow: hidden; overflow: hidden;
opacity: 0.3;
} }
.wave { .wave {
@@ -178,6 +185,7 @@ function LobsterPool({ agents, onRefresh }) {
height: 100%; height: 100%;
color: white; color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
padding: 20px;
} }
.pool-icon { .pool-icon {
@@ -207,6 +215,35 @@ function LobsterPool({ agents, onRefresh }) {
margin-top: 10px; margin-top: 10px;
} }
.dropped-agents {
margin-top: 20px;
padding: 15px;
background: rgba(255, 255, 255, 0.2);
border-radius: 8px;
max-width: 600px;
}
.dropped-title {
font-size: 1.1em;
font-weight: bold;
margin-bottom: 10px;
}
.dropped-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.dropped-agent {
background: rgba(255, 255, 255, 0.3);
padding: 5px 12px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 600;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.lobster-pool { .lobster-pool {
height: 250px; height: 250px;

View File

@@ -0,0 +1,312 @@
import React, { useState } from 'react';
const API_BASE = 'http://localhost:8000/api';
function LobsterRiver({ agents, onRefresh }) {
const [isOverRiver, setIsOverRiver] = useState(false);
const [riverAgents, setRiverAgents] = useState([]);
const [hoveredAgent, setHoveredAgent] = useState(null);
// 拖拽进入河水
const handleDragOver = (e) => {
e.preventDefault();
setIsOverRiver(true);
};
// 拖拽离开河水
const handleDragLeave = () => {
setIsOverRiver(false);
};
// 放入河水中
const handleDrop = async (e) => {
e.preventDefault();
setIsOverRiver(false);
const agentId = e.dataTransfer.getData('agentId');
const agentName = e.dataTransfer.getData('agentName');
const agentEmoji = e.dataTransfer.getData('agentEmoji');
if (agentId) {
const agent = {
id: parseInt(agentId),
name: agentName,
emoji: agentEmoji,
x: Math.random() * 80 + 10, // 随机位置 10%-90%
y: Math.random() * 60 + 20, // 随机位置 20%-80%
};
// 添加到河中
if (!riverAgents.find(a => a.id === agent.id)) {
setRiverAgents([...riverAgents, agent]);
alert(`🦐 ${agentName} 已放归河水中!🌊`);
}
// 刷新列表
if (onRefresh) {
setTimeout(() => onRefresh(), 500);
}
}
};
// 从河中拖出
const handleRiverDragStart = (e, agent) => {
e.dataTransfer.setData('agentId', agent.id.toString());
e.dataTransfer.setData('agentName', agent.name);
e.dataTransfer.setData('agentEmoji', agent.emoji);
e.dataTransfer.effectAllowed = 'move';
// 从河中移除
setRiverAgents(riverAgents.filter(a => a.id !== agent.id));
};
return (
<div className="lobster-river-container">
<h2 className="river-title">🌊 龙虾池 - 放虾归海</h2>
{/* 河水区域 */}
<div
className={`lobster-river ${isOverRiver ? 'river-active' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* 波浪动画 */}
<div className="river-waves">
<div className="wave wave-1"></div>
<div className="wave wave-2"></div>
<div className="wave wave-3"></div>
</div>
{/* 河中的龙虾 */}
<div className="river-agents">
{riverAgents.map(agent => (
<div
key={agent.id}
className="river-agent"
style={{
left: `${agent.x}%`,
top: `${agent.y}%`,
}}
draggable
onDragStart={(e) => handleRiverDragStart(e, agent)}
onMouseEnter={() => setHoveredAgent(agent.id)}
onMouseLeave={() => setHoveredAgent(null)}
title={agent.name}
>
<span className="agent-emoji">{agent.emoji}</span>
{hoveredAgent === agent.id && (
<span className="agent-name-tooltip">{agent.name}</span>
)}
</div>
))}
</div>
{/* 提示文字 */}
{riverAgents.length === 0 && (
<div className="river-hint">
<p className="hint-main">
{isOverRiver ? '🦐 松开手,放虾归海!' : '拖拽龙虾到河水中'}
</p>
<p className="hint-sub">将左侧的龙虾拖到这里放虾归海</p>
</div>
)}
</div>
{/* 河水分界线 */}
<div className="river-divider">
<div className="divider-line"></div>
<div className="divider-text"> 河岸 </div>
<div className="divider-line"></div>
</div>
<style>{`
.lobster-river-container {
margin: 20px 0 40px 0;
text-align: center;
}
.river-title {
color: #1a365d;
font-size: 1.5em;
margin-bottom: 15px;
}
.lobster-river {
width: 100%;
max-width: 1000px;
height: 200px;
margin: 0 auto;
position: relative;
overflow: hidden;
border-radius: 12px 12px 0 0;
background: linear-gradient(180deg, #4299e1 0%, #2b6cb0 50%, #1a365d 100%);
transition: all 0.3s;
cursor: pointer;
}
.lobster-river.river-active {
background: linear-gradient(180deg, #48bb78 0%, #38a169 50%, #22543d 100%);
transform: scale(1.01);
}
.river-waves {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0.3;
}
.wave {
position: absolute;
width: 200%;
height: 100px;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
animation: wave-move 6s linear infinite;
}
.wave-1 {
top: -80px;
left: -50%;
animation-delay: 0s;
}
.wave-2 {
top: -60px;
left: -50%;
animation-delay: 2s;
}
.wave-3 {
top: -40px;
left: -50%;
animation-delay: 4s;
}
@keyframes wave-move {
0% {
transform: translateX(0) rotate(0deg);
}
100% {
transform: translateX(50%) rotate(360deg);
}
}
.river-agents {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
}
.river-agent {
position: absolute;
font-size: 2em;
cursor: grab;
transition: all 0.3s;
animation: swim 3s ease-in-out infinite;
display: flex;
flex-direction: column;
align-items: center;
}
.river-agent:hover {
transform: scale(1.3);
z-index: 20;
}
@keyframes swim {
0%, 100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-10px) rotate(5deg);
}
}
.agent-emoji {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
}
.agent-name-tooltip {
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.7em;
white-space: nowrap;
z-index: 30;
}
.river-hint {
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;
}
.river-divider {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin: 0 auto;
max-width: 1000px;
}
.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;
}
@media (max-width: 768px) {
.lobster-river {
height: 150px;
}
.river-agent {
font-size: 1.5em;
}
.hint-main {
font-size: 1.2em;
}
}
`}</style>
</div>
);
}
export default LobsterRiver;

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import axios from 'axios'; import axios from 'axios';
// import LobsterPool from '../components/LobsterPool'; // TODO: Fix import import LobsterRiver from '../../components/LobsterRiver/index.js';
const API_BASE = 'http://localhost:8000/api'; const API_BASE = 'http://localhost:8000/api';
@@ -47,8 +47,9 @@ function Dashboard() {
// 拖拽处理 // 拖拽处理
const handleDragStart = (e, agent) => { const handleDragStart = (e, agent) => {
e.dataTransfer.setData('agentId', agent.id); e.dataTransfer.setData('agentId', agent.id.toString());
e.dataTransfer.setData('agentName', agent.name); e.dataTransfer.setData('agentName', agent.name);
e.dataTransfer.setData('agentEmoji', agent.emoji);
e.target.style.opacity = '0.5'; e.target.style.opacity = '0.5';
}; };
@@ -63,7 +64,10 @@ function Dashboard() {
return ( return (
<div className="dashboard"> <div className="dashboard">
<div className="dashboard-header"> <div className="dashboard-header">
<h1>🤖 Agent 舰队监控中心</h1> <div className="logo-section">
<img src="/logo.png" alt="Logo" className="site-logo" />
<h1>🦀 Agent 舰队监控中心</h1>
</div>
<button <button
className="scan-btn" className="scan-btn"
onClick={scanAgents} onClick={scanAgents}
@@ -73,18 +77,19 @@ function Dashboard() {
</button> </button>
</div> </div>
{/* TODO: 龙虾池功能待修复 */} {/* 龙虾池 - 放虾归海 */}
{/* <LobsterPool agents={agents} onRefresh={fetchAgents} /> */} <LobsterRiver agents={agents} onRefresh={fetchAgents} />
<h2 className="section-title">🦐 待归海的龙虾</h2> <h2 className="section-title">🦐 待归海的龙虾</h2>
<div className="agent-grid"> <div className="agent-grid">
{agents.map(agent => ( {agents.map(agent => (
<div <div
key={agent.id} key={agent.id}
className="agent-card" className="agent-card draggable-card"
draggable draggable
onDragStart={(e) => handleDragStart(e, agent)} onDragStart={(e) => handleDragStart(e, agent)}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
title="拖拽我到龙虾池"
> >
<div className="agent-header"> <div className="agent-header">
<span className="agent-name">{agent.emoji} {agent.name}</span> <span className="agent-name">{agent.emoji} {agent.name}</span>
@@ -100,7 +105,7 @@ function Dashboard() {
📊 Agent 详情 📊 Agent 详情
</button> </button>
</div> </div>
<div className="drag-hint">👆 拖我到龙虾池</div>
</div> </div>
))} ))}
</div> </div>
@@ -119,6 +124,25 @@ const styles = `
margin-bottom: 30px; margin-bottom: 30px;
} }
.logo-section {
display: flex;
align-items: center;
gap: 15px;
}
.site-logo {
width: 60px;
height: 60px;
object-fit: contain;
}
.dashboard h1 {
color: #1a365d;
margin: 0;
text-align: center;
font-size: 1.8em;
}
.dashboard h1 { .dashboard h1 {
color: #1a365d; color: #1a365d;
margin: 0; margin: 0;
@@ -197,6 +221,16 @@ const styles = `
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
} }
.agent-card.draggable-card {
cursor: grab;
transition: all 0.2s;
}
.agent-card.draggable-card:active {
cursor: grabbing;
transform: scale(1.02);
}
.agent-header { .agent-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;