Trae Assistant
Initial commit
c9710d1
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能机队指挥官 | Fleet Commander</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
[v-cloak] { display: none; }
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
.chat-msg { max-width: 80%; word-wrap: break-word; }
</style>
</head>
<body class="bg-slate-100 text-slate-800">
<div id="app" v-cloak class="h-screen flex flex-col">
<!-- Header -->
<header class="bg-white shadow-sm p-4 flex justify-between items-center z-20 border-b">
<div class="flex items-center gap-3">
<div class="bg-indigo-600 text-white p-2 rounded-lg">
<i class="fas fa-helicopter text-xl"></i>
</div>
<div>
<h1 class="text-xl font-bold text-slate-800 leading-tight">智能机队指挥官</h1>
<div class="text-xs text-slate-500">Fleet Commander Agent <span class="bg-green-100 text-green-700 px-1 rounded ml-1">v2.0</span></div>
</div>
</div>
<div class="flex gap-4 text-sm text-slate-600 items-center">
<div class="hidden md:flex items-center gap-2 px-3 py-1 bg-slate-50 rounded-full border">
<div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
<span>系统在线</span>
</div>
<div class="hidden md:block">CPU: 12%</div>
</div>
</header>
<!-- Main Content -->
<main class="flex-1 flex overflow-hidden flex-col md:flex-row">
<!-- Left: Map Area -->
<div class="flex-1 relative bg-slate-200 flex flex-col" id="map-container">
<div id="fleet-map" class="flex-1 w-full h-full min-h-[300px]"></div>
<!-- Map Overlay Stats -->
<div class="absolute top-4 left-4 bg-white/90 p-4 rounded-xl shadow-lg backdrop-blur-sm z-10 border border-white/50">
<div class="text-xs text-slate-500 mb-1 font-medium uppercase tracking-wider">活跃单位</div>
<div class="text-3xl font-black text-indigo-600 flex items-baseline gap-1">
${ stats.active }
<span class="text-sm font-normal text-slate-400">/ ${ stats.total }</span>
</div>
</div>
<!-- Alert Overlay (Bottom Left) -->
<div class="absolute bottom-4 left-4 right-4 md:right-auto md:w-96 max-h-48 overflow-y-auto bg-black/70 text-white p-3 rounded-lg shadow-lg z-10 backdrop-blur-md text-xs font-mono">
<div v-for="(alert, i) in alerts.slice(0, 5)" :key="i" class="mb-1.5 flex gap-2">
<span class="opacity-50 text-cyan-400">[${ alert.time }]</span>
<span :class="{'text-red-400': alert.level==='error', 'text-yellow-400': alert.level==='warning', 'text-green-400': alert.level==='success'}">
${ alert.message }
</span>
</div>
<div v-if="alerts.length === 0" class="opacity-50 italic">暂无警报数据...</div>
</div>
</div>
<!-- Right: Sidebar Control Panel -->
<aside class="w-full md:w-[400px] bg-white shadow-2xl z-20 flex flex-col border-l border-slate-200 h-[60%] md:h-auto">
<!-- Tabs -->
<div class="flex border-b text-sm font-medium">
<button @click="currentTab = 'control'" :class="{'text-indigo-600 border-b-2 border-indigo-600 bg-indigo-50': currentTab === 'control'}" class="flex-1 py-3 hover:bg-slate-50 transition-colors">
<i class="fas fa-gamepad mr-1"></i> 控制
</button>
<button @click="currentTab = 'fleet'" :class="{'text-indigo-600 border-b-2 border-indigo-600 bg-indigo-50': currentTab === 'fleet'}" class="flex-1 py-3 hover:bg-slate-50 transition-colors">
<i class="fas fa-robot mr-1"></i> 机队
</button>
<button @click="currentTab = 'ai'" :class="{'text-indigo-600 border-b-2 border-indigo-600 bg-indigo-50': currentTab === 'ai'}" class="flex-1 py-3 hover:bg-slate-50 transition-colors">
<i class="fas fa-brain mr-1"></i> AI 助手
</button>
</div>
<!-- Content Area -->
<div class="flex-1 overflow-hidden relative bg-slate-50">
<!-- Tab: Control -->
<div v-if="currentTab === 'control'" class="h-full overflow-y-auto p-4 space-y-4">
<!-- Quick Stats -->
<div class="grid grid-cols-3 gap-3">
<div class="bg-white p-3 rounded-lg shadow-sm border border-slate-100 text-center">
<div class="text-xs text-slate-500">总数</div>
<div class="text-xl font-bold text-slate-700">${ stats.total }</div>
</div>
<div class="bg-white p-3 rounded-lg shadow-sm border border-slate-100 text-center">
<div class="text-xs text-green-600">充电中</div>
<div class="text-xl font-bold text-green-600">${ stats.charging }</div>
</div>
<div class="bg-white p-3 rounded-lg shadow-sm border border-slate-100 text-center">
<div class="text-xs text-red-600">故障</div>
<div class="text-xl font-bold text-red-600">${ stats.error }</div>
</div>
</div>
<!-- Manual Task -->
<div class="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
<h3 class="text-sm font-bold mb-3 text-slate-700 flex items-center gap-2">
<i class="fas fa-tasks text-indigo-500"></i> 手动任务
</h3>
<div class="flex gap-2">
<input v-model="newTask" @keyup.enter="assignTask" type="text" placeholder="输入任务指令 (如: 巡逻 A 区)" class="flex-1 border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500">
<button @click="assignTask" class="bg-indigo-600 text-white px-4 py-2 rounded-md text-sm hover:bg-indigo-700 transition-colors">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
<!-- File Upload -->
<div class="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
<h3 class="text-sm font-bold mb-3 text-slate-700 flex items-center gap-2">
<i class="fas fa-file-upload text-indigo-500"></i> 任务文件上传
</h3>
<div class="border-2 border-dashed border-slate-300 rounded-lg p-4 text-center hover:bg-slate-50 transition-colors cursor-pointer relative">
<input type="file" @change="handleFileUpload" class="absolute inset-0 opacity-0 cursor-pointer">
<div v-if="!uploading">
<i class="fas fa-cloud-upload-alt text-2xl text-slate-400 mb-2"></i>
<p class="text-xs text-slate-500">点击或拖拽上传 JSON/TXT 任务文件</p>
</div>
<div v-else class="text-indigo-600">
<i class="fas fa-spinner fa-spin"></i> 上传解析中...
</div>
</div>
</div>
</div>
<!-- Tab: Fleet -->
<div v-if="currentTab === 'fleet'" class="h-full overflow-y-auto p-4 space-y-2">
<div v-for="bot in robots" :key="bot.id" class="bg-white p-3 rounded-lg shadow-sm border border-slate-200 flex items-center justify-between hover:shadow-md transition-shadow">
<div>
<div class="flex items-center gap-2 mb-1">
<span class="font-bold text-sm text-slate-700">${ bot.id }</span>
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-slate-100 text-slate-500 border">${ bot.type }</span>
</div>
<div class="flex items-center gap-3 text-xs text-slate-500">
<span :class="{'text-green-500': bot.battery > 20, 'text-red-500': bot.battery <= 20}">
<i class="fas fa-battery-three-quarters"></i> ${ Math.round(bot.battery) }%
</span>
<span class="flex items-center gap-1">
<div class="w-1.5 h-1.5 rounded-full" :class="getStatusColorBg(bot.status)"></div>
${ getStatusText(bot.status) }
</span>
</div>
</div>
<button v-if="bot.status === 'error'" @click="fixBot(bot.id)" class="bg-red-50 text-red-600 border border-red-200 text-xs px-3 py-1.5 rounded-md hover:bg-red-100 transition-colors">
<i class="fas fa-wrench mr-1"></i> 修复
</button>
</div>
</div>
<!-- Tab: AI Assistant -->
<div v-if="currentTab === 'ai'" class="h-full flex flex-col bg-white">
<div class="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50" ref="chatContainer">
<div v-for="(msg, i) in chatHistory" :key="i" class="flex" :class="msg.role === 'user' ? 'justify-end' : 'justify-start'">
<div class="chat-msg p-3 rounded-lg text-sm shadow-sm"
:class="msg.role === 'user' ? 'bg-indigo-600 text-white rounded-br-none' : 'bg-white border border-slate-200 text-slate-700 rounded-bl-none'">
<div v-if="msg.role === 'assistant'" class="text-xs text-indigo-500 font-bold mb-1 flex items-center gap-1">
<i class="fas fa-robot"></i> 指挥官助手
</div>
<div class="whitespace-pre-wrap">${ msg.content }</div>
</div>
</div>
<div v-if="isThinking" class="flex justify-start">
<div class="chat-msg p-3 rounded-lg text-sm bg-white border border-slate-200 text-slate-500 rounded-bl-none">
<i class="fas fa-circle-notch fa-spin mr-1"></i> 思考中...
</div>
</div>
</div>
<div class="p-3 border-t bg-white">
<div class="flex gap-2">
<input v-model="chatInput" @keyup.enter="sendChat" type="text" placeholder="询问机队状态或寻求建议..." class="flex-1 border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-slate-50">
<button @click="sendChat" :disabled="isThinking" class="bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm hover:bg-indigo-700 transition-colors disabled:opacity-50">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
</div>
</aside>
</main>
</div>
<script>
const { createApp, ref, onMounted, nextTick, watch } = Vue;
createApp({
delimiters: ['${', '}'],
setup() {
const robots = ref([]);
const alerts = ref([]);
const stats = ref({ total: 0, active: 0, charging: 0, error: 0 });
const newTask = ref('');
const currentTab = ref('control');
const chatInput = ref('');
const chatHistory = ref([
{ role: 'assistant', content: '指挥官您好!我是您的智能机队助手。您可以询问我关于机队状态的问题,或者让我协助分析异常。' }
]);
const isThinking = ref(false);
const uploading = ref(false);
const chatContainer = ref(null);
let mapChart = null;
// --- Map Logic ---
const initMap = () => {
const el = document.getElementById('fleet-map');
if (!el) return;
mapChart = echarts.init(el);
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
formatter: (params) => {
const bot = params.data.bot;
return `
<div class="font-bold">${bot.id}</div>
<div>状态: ${getStatusText(bot.status)}</div>
<div>电量: ${Math.round(bot.battery)}%</div>
<div>坐标: (${Math.round(bot.x)}, ${Math.round(bot.y)})</div>
`;
}
},
grid: { top: 20, bottom: 20, left: 20, right: 20 },
xAxis: { min: 0, max: 100, show: false },
yAxis: { min: 0, max: 100, show: false },
series: [{
type: 'scatter',
symbolSize: 30,
data: [],
itemStyle: {
color: (params) => {
const status = params.data.status;
if (status === 'error') return '#ef4444';
if (status === 'charging') return '#22c55e';
if (status === 'active') return '#4f46e5';
return '#94a3b8';
},
shadowBlur: 10,
shadowColor: 'rgba(0,0,0,0.2)'
},
label: {
show: true,
formatter: '{b}',
position: 'top',
color: '#333',
fontSize: 11,
fontWeight: 'bold'
}
}]
};
mapChart.setOption(option);
window.addEventListener('resize', () => mapChart.resize());
};
const updateMap = () => {
if (!mapChart) return;
const data = robots.value.map(bot => ({
name: bot.id,
value: [bot.x, bot.y],
status: bot.status,
bot: bot
}));
mapChart.setOption({
series: [{ data }]
});
};
// --- API Logic ---
const fetchTelemetry = async () => {
try {
const res = await fetch('/api/telemetry');
const data = await res.json();
robots.value = data.robots;
alerts.value = data.alerts;
stats.value = data.stats;
updateMap();
} catch (e) {
console.error("Telemetry error:", e);
}
};
const assignTask = async () => {
if (!newTask.value) return;
try {
await fetch('/api/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'task', description: newTask.value })
});
newTask.value = '';
fetchTelemetry();
} catch (e) {
alert("任务下发失败: " + e.message);
}
};
const fixBot = async (botId) => {
try {
await fetch('/api/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'fix', bot_id: botId })
});
fetchTelemetry();
} catch (e) {
alert("修复指令失败: " + e.message);
}
};
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
uploading.value = true;
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await res.json();
alert(data.message);
} catch (e) {
alert("上传失败: " + e.message);
} finally {
uploading.value = false;
event.target.value = ''; // Reset input
}
};
const sendChat = async () => {
if (!chatInput.value.trim() || isThinking.value) return;
const msg = chatInput.value;
chatHistory.value.push({ role: 'user', content: msg });
chatInput.value = '';
isThinking.value = true;
scrollToBottom();
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: msg })
});
const data = await res.json();
if (data.success) {
chatHistory.value.push({ role: 'assistant', content: data.reply });
} else {
chatHistory.value.push({ role: 'assistant', content: "API 错误: " + data.reply });
}
} catch (e) {
chatHistory.value.push({ role: 'assistant', content: "网络错误,请稍后再试。" });
} finally {
isThinking.value = false;
scrollToBottom();
}
};
const scrollToBottom = () => {
nextTick(() => {
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
}
});
};
// --- Helpers ---
const getStatusText = (status) => {
const map = { 'idle': '待命', 'active': '任务中', 'charging': '充电中', 'error': '故障' };
return map[status] || status;
};
const getStatusColorBg = (status) => {
if (status === 'error') return 'bg-red-500';
if (status === 'charging') return 'bg-green-500';
if (status === 'active') return 'bg-indigo-500';
return 'bg-slate-400';
};
onMounted(() => {
initMap();
fetchTelemetry();
setInterval(fetchTelemetry, 1000); // Poll every second
});
return {
robots, alerts, stats, newTask, currentTab,
chatInput, chatHistory, isThinking, uploading, chatContainer,
assignTask, fixBot, handleFileUpload, sendChat,
getStatusText, getStatusColorBg
};
}
}).mount('#app');
</script>
</body>
</html>