feat: 添加放虾归海功能(龙虾河)🦐🌊
🎨 新设计: - 龙虾河:200px 高,蓝色渐变背景 - 波浪动画:三层半透明波浪滚动效果 - 河水分界线:贯穿的蓝线 + '河岸'文字 - 河中的龙虾:随机位置,游动动画 🦐 交互玩法: 1. 拖拽龙虾卡片到河水中 2. 龙虾变成 emoji 在河中游动 3. 鼠标悬停显示名字 tooltip 4. 可以从河中拖回下方列表 ✨ 功能特性: - 龙虾在河中随机分布(10%-90% 位置) - 游动动画(上下浮动 + 左右摇摆) - 悬停显示名字 - 拖拽出河水后从河中移除 - 成功提示:'XX 已放归河水中!🌊' 🦀 Logo 更新: - 添加 favicon(浏览器标签页图标) - Dashboard 顶部显示 logo - 标题 emoji 更新为 🦀 🎯 组件重构: - LobsterPool → LobsterRiver(更贴切) - 简化组件结构 - 优化动画效果 🦸 感谢北极星 ⭐ 的创意! '放虾归海' - 太好玩了!😄
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
BIN
code/frontend/public/logo.png
Normal file
BIN
code/frontend/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 625 KiB |
@@ -5,11 +5,13 @@ const API_BASE = 'http://localhost:8000/api';
|
||||
function LobsterPool({ agents, onRefresh }) {
|
||||
const [isDragging, setIsDragging] = useState(null);
|
||||
const [isOverPool, setIsOverPool] = useState(false);
|
||||
const [droppedAgents, setDroppedAgents] = useState([]);
|
||||
|
||||
// 开始拖拽
|
||||
const handleDragStart = (e, 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';
|
||||
};
|
||||
|
||||
@@ -36,30 +38,20 @@ function LobsterPool({ agents, onRefresh }) {
|
||||
setIsOverPool(false);
|
||||
|
||||
const agentId = e.dataTransfer.getData('agentId');
|
||||
const agent = agents.find(a => a.id === parseInt(agentId));
|
||||
const agentName = e.dataTransfer.getData('agentName');
|
||||
|
||||
if (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('❌ 放虾失败:' + error.message);
|
||||
if (agentId) {
|
||||
// 添加到已放入列表
|
||||
const agent = agents.find(a => a.id === parseInt(agentId));
|
||||
if (agent && !droppedAgents.find(a => a.id === agent.id)) {
|
||||
setDroppedAgents([...droppedAgents, agent]);
|
||||
}
|
||||
|
||||
alert(`🦐 ${agentName} 已放虾归海!🌊`);
|
||||
|
||||
// 刷新列表(可选:这里可以调用 API 真正同步到数据库)
|
||||
if (onRefresh) {
|
||||
setTimeout(() => onRefresh(), 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -88,6 +80,20 @@ function LobsterPool({ agents, onRefresh }) {
|
||||
{isOverPool ? '🦐 松开手,放虾归海!' : '拖拽龙虾到池中'}
|
||||
</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>
|
||||
|
||||
@@ -114,6 +120,7 @@ function LobsterPool({ agents, onRefresh }) {
|
||||
border: 3px solid #4299e1;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(180deg, #4299e1 0%, #2b6cb0 100%);
|
||||
}
|
||||
|
||||
.lobster-pool.pool-active {
|
||||
@@ -128,8 +135,8 @@ function LobsterPool({ agents, onRefresh }) {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(180deg, #4299e1 0%, #2b6cb0 100%);
|
||||
overflow: hidden;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.wave {
|
||||
@@ -178,6 +185,7 @@ function LobsterPool({ agents, onRefresh }) {
|
||||
height: 100%;
|
||||
color: white;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.pool-icon {
|
||||
@@ -207,6 +215,35 @@ function LobsterPool({ agents, onRefresh }) {
|
||||
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) {
|
||||
.lobster-pool {
|
||||
height: 250px;
|
||||
|
||||
312
code/frontend/src/components/LobsterRiver/index.js
Normal file
312
code/frontend/src/components/LobsterRiver/index.js
Normal 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;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
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';
|
||||
|
||||
@@ -47,8 +47,9 @@ function Dashboard() {
|
||||
|
||||
// 拖拽处理
|
||||
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('agentEmoji', agent.emoji);
|
||||
e.target.style.opacity = '0.5';
|
||||
};
|
||||
|
||||
@@ -63,7 +64,10 @@ function Dashboard() {
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<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
|
||||
className="scan-btn"
|
||||
onClick={scanAgents}
|
||||
@@ -73,18 +77,19 @@ function Dashboard() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* TODO: 龙虾池功能待修复 */}
|
||||
{/* <LobsterPool agents={agents} onRefresh={fetchAgents} /> */}
|
||||
{/* 龙虾池 - 放虾归海 */}
|
||||
<LobsterRiver agents={agents} onRefresh={fetchAgents} />
|
||||
|
||||
<h2 className="section-title">🦐 待归海的龙虾</h2>
|
||||
<div className="agent-grid">
|
||||
{agents.map(agent => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="agent-card"
|
||||
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>
|
||||
@@ -100,7 +105,7 @@ function Dashboard() {
|
||||
📊 Agent 详情
|
||||
</button>
|
||||
</div>
|
||||
<div className="drag-hint">👆 拖我到龙虾池</div>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -119,6 +124,25 @@ const styles = `
|
||||
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 {
|
||||
color: #1a365d;
|
||||
margin: 0;
|
||||
@@ -197,6 +221,16 @@ const styles = `
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
Reference in New Issue
Block a user