Files
X-Agents/web/src/views/Dashboard.vue
DESKTOP-72TV0V4\caoxiaozhu 6d5ae6c604 Add Database page with new connection feature
- Reorganize project structure: move frontend to web/ directory
- Add Database page with connection list (name, type, subtables, status, created, actions)
- Integrate Element Plus for UI components with dark theme support
- Add Quicksand font for rounded UI design
- Configure root package.json to run frontend from web/ directory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:49:46 +08:00

440 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 显示的数字(用于动画)
const displayStats = ref({
activeAgents: 0,
activeMCP: 0,
activeModels: 0,
requests: 0,
agentsCalls: 0,
mcpCalls: 0,
modelRequests: 0,
})
// 目标数字
const targetStats = {
activeAgents: 3,
activeMCP: 21,
activeModels: 13,
requests: 36,
agentsCalls: 3,
mcpCalls: 21,
modelRequests: 13,
}
// 数字滚动动画函数
const animateNumber = (key: keyof typeof displayStats.value, target: number) => {
const duration = 2000
const startTime = Date.now()
const startValue = 0
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
// 使用easeOut曲线
const eased = 1 - Math.pow(1 - progress, 3)
displayStats.value[key] = Math.floor(startValue + (target - startValue) * eased)
if (progress < 1) {
requestAnimationFrame(animate)
} else {
displayStats.value[key] = target
}
}
animate()
}
onMounted(() => {
// 页面加载后依次动画每个数字
setTimeout(() => animateNumber('activeAgents', targetStats.activeAgents), 0)
setTimeout(() => animateNumber('activeMCP', targetStats.activeMCP), 300)
setTimeout(() => animateNumber('activeModels', targetStats.activeModels), 600)
// deployment insights 数字动画延迟900ms在顶部stats之后
setTimeout(() => animateNumber('requests', targetStats.requests), 900)
setTimeout(() => animateNumber('agentsCalls', targetStats.agentsCalls), 1000)
setTimeout(() => animateNumber('mcpCalls', targetStats.mcpCalls), 1100)
setTimeout(() => animateNumber('modelRequests', targetStats.modelRequests), 1200)
})
// Agents列表
const agents = ref([
{ name: 'template-google-adk-api', count: 1, color: 'bg-primary-yellow', status: 'success' },
{ name: 'mcp-google-adk-api', count: 1, color: 'bg-primary-cyan', status: 'error' },
{ name: 'template-openai-api', count: 1, color: 'bg-primary-purple', status: 'error' },
])
// MCP Servers列表
const mcpServers = ref([
{ name: 'linear-demo', count: 15, color: 'bg-primary-yellow' },
{ name: 'google-maps', count: 4, color: 'bg-primary-cyan' },
{ name: 'explorer-mcp', count: 2, color: 'bg-primary-purple' },
])
// Models列表
const models = ref([
{ name: 'gpt-40-2024-08-12', count: 2, color: 'bg-primary-yellow' },
{ name: 'cerebras-sandbox', count: 6, color: 'bg-primary-cyan' },
{ name: 'sandbox-openai', count: 5, color: 'bg-primary-purple' },
])
// 图表数据
const chartData = ref([
{ time: '3:02 PM', agents: 1, mcp: 2, models: 1.5 },
{ time: '3:07 PM', agents: 2, mcp: 2.5, models: 2 },
{ time: '3:12 PM', agents: 2.5, mcp: 5, models: 4 },
{ time: '3:17 PM', agents: 2.5, mcp: 3, models: 2 },
{ time: '3:22 PM', agents: 1.5, mcp: 2.5, models: 1.5 },
{ time: '3:27 PM', agents: 1.5, mcp: 3, models: 2.5 },
{ time: '3:32 PM', agents: 1, mcp: 2, models: 2.5 },
{ time: '3:37 PM', agents: 2.5, mcp: 8, models: 3 },
{ time: '3:42 PM', agents: 1, mcp: 5, models: 2.5 },
{ time: '3:47 PM', agents: 2.5, mcp: 3, models: 2 },
])
// Top 10请求
const topRequests = ref([
{ name: 'gpt-40-2024-08-12', type: 'cube', count: 7 },
{ name: 'google-maps', type: 'code', count: 4 },
{ name: 'explorer-mcp', type: 'code', count: 2 },
{ name: 'template-google-adk-api', type: 'cube', count: 4 },
{ name: 'linear-demo', type: 'cube', count: 2 },
{ name: 'cerebras-sandbox', type: 'code', count: 1 },
{ name: 'sandbox-openai', type: 'cube', count: 2 },
])
// What's new
const whatsNew = ref([
{ title: 'New framework supported: PydanticAI', desc: 'Added support for PydanticAI framework', date: '2025-04-12' },
{ title: 'New framework supported: Google ADK', desc: 'Added support for Google ADK (Agent Development Kit) framework', date: '2025-04-07' },
{ title: 'Improved Analytics Dashboard', desc: 'Enhanced real-time monitoring with faster data refresh', date: '2025-04-15' },
])
// Recent requests
const recentRequests = ref([
{ name: 'linear-demo', type: 'cube', time: '21 hours', status: 'success' },
{ name: 'myagent', type: 'robot', time: '21 hours', status: 'success' },
{ name: 'linear-demo', type: 'cube', time: '21 hours', status: 'success' },
{ name: 'gpt-40', type: 'code', time: '21 hours', status: 'success' },
{ name: 'linear-demo', type: 'cube', time: '21 hours', status: 'success' },
])
// 开关状态
const agentErrorEnabled = ref(true)
const mcpErrorEnabled = ref(false)
const modelErrorEnabled = ref(false)
// Top requests标签切换
const topRequestsTab = ref<'general' | 'errors'>('general')
</script>
<template>
<!-- 主内容区域 -->
<div class="p-6 min-h-screen">
<!-- 顶部导航与日期选择区 -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-gauge text-gray-400"></i>
<span class="font-medium">Dashboard</span>
</div>
<div class="flex items-center gap-4">
<span class="text-gray-400">Date Range</span>
<div class="flex items-center gap-2 bg-dark-600 rounded-lg px-3 py-2">
<span>10 December</span>
<span class="text-gray-400">To</span>
<span>12 December</span>
<i class="fa-solid fa-calendar text-gray-400"></i>
</div>
<button class="text-primary-orange hover:text-orange-400 transition-colors">Clear</button>
</div>
</div>
<!-- 卡片网格布局 -->
<div class="grid grid-cols-3 gap-6">
<!-- 第一行3个状态卡片 -->
<!-- Active Agents 卡片 -->
<div class="bg-dark-700 rounded-xl p-5">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">Active Agents</h3>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-400">Errors</span>
<!-- 开启状态开关 -->
<div
class="w-10 h-5 rounded-full relative cursor-pointer transition-colors"
:class="agentErrorEnabled ? 'bg-primary-orange' : 'bg-dark-500'"
@click="agentErrorEnabled = !agentErrorEnabled"
>
<div
class="absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform"
:class="agentErrorEnabled ? 'right-0.5' : 'left-0.5'"
></div>
</div>
</div>
</div>
<div class="text-4xl font-bold mb-4">{{ displayStats.activeAgents }}</div>
<!-- 进度条 - 三个同时动画 -->
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4 relative">
<!-- 黄色 -->
<div class="absolute left-0 top-0 h-full bg-primary-yellow progress-bar" style="--target-width: 33%"></div>
<!-- 蓝色 -->
<div class="absolute top-0 h-full bg-primary-cyan progress-bar" style="left: 33%; --target-width: 33%"></div>
<!-- 紫色 -->
<div class="absolute top-0 h-full bg-primary-purple progress-bar" style="left: 66%; --target-width: 34%"></div>
</div>
<!-- 明细列表 -->
<ul class="space-y-2">
<li v-for="agent in agents" :key="agent.name" class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm" :class="agent.color"></span>
<span class="text-sm text-gray-300">{{ agent.name }}</span>
<span v-if="agent.status === 'error'" class="bg-primary-danger text-white text-xs px-1.5 py-0.5 rounded">Error</span>
</div>
<span class="text-sm">{{ agent.count }}</span>
</li>
</ul>
</div>
<!-- Active MCP Servers 卡片 -->
<div class="bg-dark-700 rounded-xl p-5">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">Active MCP Servers</h3>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-400">Errors</span>
<div
class="w-10 h-5 rounded-full relative cursor-pointer transition-colors"
:class="mcpErrorEnabled ? 'bg-primary-orange' : 'bg-dark-500'"
@click="mcpErrorEnabled = !mcpErrorEnabled"
>
<div
class="absolute top-0.5 w-4 h-4 rounded-full transition-transform"
:class="mcpErrorEnabled ? 'right-0.5 bg-white' : 'left-0.5 bg-gray-400'"
></div>
</div>
</div>
</div>
<div class="text-4xl font-bold mb-4">{{ displayStats.activeMCP }}</div>
<!-- 进度条 - 三个同时动画 -->
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4 relative">
<div class="absolute left-0 top-0 h-full bg-primary-yellow progress-bar" style="--target-width: 71%"></div>
<div class="absolute top-0 h-full bg-primary-cyan progress-bar" style="left: 71%; --target-width: 19%"></div>
<div class="absolute top-0 h-full bg-primary-purple progress-bar" style="left: 90%; --target-width: 10%"></div>
</div>
<!-- 明细列表 -->
<ul class="space-y-2">
<li v-for="server in mcpServers" :key="server.name" class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm" :class="server.color"></span>
<span class="text-sm text-gray-300">{{ server.name }}</span>
</div>
<span class="text-sm">{{ server.count }}</span>
</li>
</ul>
</div>
<!-- Active Models 卡片 -->
<div class="bg-dark-700 rounded-xl p-5">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">Active Models</h3>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-400">Errors</span>
<div
class="w-10 h-5 rounded-full relative cursor-pointer transition-colors"
:class="modelErrorEnabled ? 'bg-primary-orange' : 'bg-dark-500'"
@click="modelErrorEnabled = !modelErrorEnabled"
>
<div
class="absolute top-0.5 w-4 h-4 rounded-full transition-transform"
:class="modelErrorEnabled ? 'right-0.5 bg-white' : 'left-0.5 bg-gray-400'"
></div>
</div>
</div>
</div>
<div class="text-4xl font-bold mb-4">{{ displayStats.activeModels }}</div>
<!-- 进度条 - 三个同时动画 -->
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4 relative">
<div class="absolute left-0 top-0 h-full bg-primary-yellow progress-bar" style="--target-width: 46%"></div>
<div class="absolute top-0 h-full bg-primary-cyan progress-bar" style="left: 46%; --target-width: 15%"></div>
<div class="absolute top-0 h-full bg-primary-purple progress-bar" style="left: 61%; --target-width: 39%"></div>
</div>
<!-- 明细列表 -->
<ul class="space-y-2">
<li v-for="model in models" :key="model.name" class="flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm" :class="model.color"></span>
<span class="text-sm text-gray-300">{{ model.name }}</span>
</div>
<span class="text-sm">{{ model.count }}</span>
</li>
</ul>
</div>
<!-- 第二行 -->
<!-- All deployment request Insights 卡片跨2列 -->
<div class="bg-dark-700 rounded-xl p-5 col-span-2">
<h3 class="font-semibold text-lg mb-4">All deployment request Insights</h3>
<!-- 数据概览 -->
<div class="grid grid-cols-4 gap-4 mb-6">
<div>
<div class="text-sm text-gray-400 mb-1">Requests</div>
<div class="text-2xl font-bold">{{ displayStats.requests }}</div>
</div>
<div>
<div class="text-sm text-gray-400 mb-1">Agents calls</div>
<div class="text-2xl font-bold">{{ displayStats.agentsCalls }}</div>
</div>
<div>
<div class="text-sm text-gray-400 mb-1">MCP servers calls</div>
<div class="text-2xl font-bold">{{ displayStats.mcpCalls }}</div>
</div>
<div>
<div class="text-sm text-gray-400 mb-1">Models requests</div>
<div class="text-2xl font-bold">{{ displayStats.modelRequests }}</div>
</div>
</div>
<!-- 纯CSS模拟柱状图 -->
<div class="relative h-52 w-full">
<!-- 绘图区网格线+Y轴+柱子 预留底部20px给时间标签 -->
<div class="relative h-[calc(100%-20px)] w-full">
<!-- 横向网格线 -->
<div class="absolute left-0 top-0 w-full h-full z-0">
<div class="absolute w-full h-[1px] bg-white/[0.06] top-0"></div>
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[25%]"></div>
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[50%]"></div>
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[75%]"></div>
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[100%]"></div>
</div>
<!-- Y轴刻度 -->
<div class="absolute left-0 top-0 h-full flex flex-col justify-between text-xs text-gray-500 z-10">
<span>8</span>
<span>6</span>
<span>4</span>
<span>2</span>
<span>0</span>
</div>
<!-- 柱状图容器 柱子底部对齐0网格线 -->
<div class="ml-6 h-full flex items-end justify-between gap-1 z-10 relative">
<div
v-for="item in chartData"
:key="item.time"
class="flex items-end gap-1 h-full w-full justify-center"
>
<!-- 所有柱子同时动画 -->
<div
class="w-3 bg-primary-yellow rounded-t-sm chart-bar"
:style="{ height: (item.mcp / 8 * 100) + '%' }"
></div>
<div
class="w-3 bg-primary-cyan rounded-t-sm chart-bar"
:style="{ height: (item.models / 8 * 100) + '%' }"
></div>
<div
class="w-3 bg-primary-purple rounded-t-sm chart-bar"
:style="{ height: (item.agents / 8 * 100) + '%' }"
></div>
</div>
</div>
</div>
<!-- 时间标签区 -->
<div class="ml-6 w-full flex justify-between gap-1 mt-1">
<span v-for="item in chartData" :key="item.time" class="text-xs text-gray-500 w-full text-center">{{ item.time }}</span>
</div>
</div>
<!-- 图例 -->
<div class="flex justify-center gap-6 mt-4">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-purple"></span>
<span class="text-xs text-gray-400">Agents calls</span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-yellow"></span>
<span class="text-xs text-gray-400">MCP servers calls</span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm bg-primary-cyan"></span>
<span class="text-xs text-gray-400">Models requests</span>
</div>
</div>
</div>
<!-- Top 10 requests 卡片 -->
<div class="bg-dark-700 rounded-xl p-5">
<h3 class="font-semibold text-lg mb-4">Top 10 requests</h3>
<!-- 标签切换 -->
<div class="flex mb-4 border border-dark-500 rounded-lg overflow-hidden">
<button
class="flex-1 py-2 text-sm font-medium transition-colors"
:class="topRequestsTab === 'general' ? 'bg-dark-500 text-white' : 'bg-dark-600 text-gray-400 hover:text-white'"
@click="topRequestsTab = 'general'"
>
General
</button>
<button
class="flex-1 py-2 text-sm font-medium transition-colors"
:class="topRequestsTab === 'errors' ? 'bg-dark-500 text-white' : 'bg-dark-600 text-gray-400 hover:text-white'"
@click="topRequestsTab = 'errors'"
>
Errors
</button>
</div>
<!-- 列表 -->
<ul class="space-y-3">
<li v-for="req in topRequests" :key="req.name" class="flex justify-between items-center">
<div class="flex items-center gap-2">
<i :class="['fa-solid', req.type === 'cube' ? 'fa-cube' : 'fa-code', 'text-gray-400']"></i>
<span class="text-sm text-gray-300">{{ req.name }}</span>
</div>
<span class="text-sm">{{ req.count }}</span>
</li>
</ul>
</div>
<!-- 第三行 -->
<!-- What's new 卡片跨2列 -->
<div class="bg-dark-700 rounded-xl p-5 col-span-2">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">What's new</h3>
<a href="#" class="text-primary-orange text-sm flex items-center gap-1 hover:text-orange-400 transition-colors">
Full change log
<i class="fa-solid fa-arrow-up-right-from-square"></i>
</a>
</div>
<p class="text-sm text-gray-400 mb-4">Stay up to date with our latest feature and improvements</p>
<!-- 更新列表 -->
<ul class="space-y-4">
<li v-for="item in whatsNew" :key="item.title" class="flex justify-between items-start">
<div>
<h4 class="font-medium mb-1">{{ item.title }}</h4>
<p class="text-sm text-gray-400">{{ item.desc }}</p>
</div>
<span class="text-xs text-gray-500">{{ item.date }}</span>
</li>
</ul>
</div>
<!-- Recent requests (10) 卡片 -->
<div class="bg-dark-700 rounded-xl p-5">
<h3 class="font-semibold text-lg mb-4">Recent requests (10)</h3>
<!-- 列表 -->
<ul class="space-y-3">
<li v-for="(req, index) in recentRequests" :key="index" class="flex items-center gap-3">
<i :class="['fa-solid', req.type === 'cube' ? 'fa-cube' : req.type === 'robot' ? 'fa-robot' : 'fa-code', 'text-gray-400']"></i>
<div class="flex-1">
<div class="flex justify-between items-center">
<span class="text-sm font-medium">{{ req.name }}</span>
<span class="text-xs text-gray-500">in {{ req.time }}</span>
</div>
<div class="flex items-center gap-1 text-xs text-primary-success">
<i class="fa-solid fa-circle-check"></i>
<span>{{ req.status }}</span>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>