679 lines
18 KiB
Vue
679 lines
18 KiB
Vue
<template>
|
|
<n-config-provider :theme="theme">
|
|
<n-global-style />
|
|
<n-message-provider>
|
|
<n-dialog-provider>
|
|
<n-notification-provider>
|
|
<div class="app">
|
|
<!-- 侧边栏 -->
|
|
<aside class="sidebar">
|
|
<div class="logo">
|
|
<h2>MCP Client</h2>
|
|
</div>
|
|
|
|
<nav class="nav">
|
|
<n-menu
|
|
v-model:value="activeRoute"
|
|
:options="menuOptions"
|
|
:collapsed="false"
|
|
@update:value="handleMenuSelect"
|
|
/>
|
|
</nav>
|
|
</aside>
|
|
|
|
<!-- 主内容区 -->
|
|
<main class="main-content">
|
|
<!-- 头部 -->
|
|
<header class="header">
|
|
<div class="header-left">
|
|
<h1>{{ currentPageTitle }}</h1>
|
|
</div>
|
|
|
|
<div class="header-right">
|
|
<!-- 连接状态 -->
|
|
<div class="connection-status">
|
|
<n-icon
|
|
:component="connectedServers.length > 0 ? WifiIcon : WifiOffIcon"
|
|
:color="connectedServers.length > 0 ? '#52c41a' : '#ff4d4f'"
|
|
size="18"
|
|
/>
|
|
<span>{{ connectedServers.length }} / {{ servers.length }} 服务器已连接</span>
|
|
</div>
|
|
|
|
<!-- 主题切换 -->
|
|
<n-button
|
|
quaternary
|
|
circle
|
|
@click="toggleTheme"
|
|
>
|
|
<template #icon>
|
|
<n-icon :component="isDark ? SunIcon : MoonIcon" />
|
|
</template>
|
|
</n-button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 页面内容 -->
|
|
<div class="page-content">
|
|
<!-- 仪表盘 -->
|
|
<div v-if="activeRoute === 'dashboard'" class="dashboard">
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-icon">
|
|
<n-icon :component="ServerIcon" size="24" />
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-number">{{ servers.length }}</div>
|
|
<div class="stat-label">MCP 服务器</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon connected">
|
|
<n-icon :component="PlugIcon" size="24" />
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-number">{{ connectedServers.length }}</div>
|
|
<div class="stat-label">已连接</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon tools">
|
|
<n-icon :component="ToolIcon" size="24" />
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-number">{{ availableTools.length }}</div>
|
|
<div class="stat-label">可用工具</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon llm">
|
|
<n-icon :component="BrainIcon" size="24" />
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-number">{{ llmConfig.enabled ? 'ON' : 'OFF' }}</div>
|
|
<div class="stat-label">LLM 助手</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 快速操作 -->
|
|
<div class="quick-actions">
|
|
<h3>快速操作</h3>
|
|
<div class="action-buttons">
|
|
<n-button type="primary" @click="showAddServerModal = true">
|
|
<template #icon>
|
|
<n-icon :component="PlusIcon" />
|
|
</template>
|
|
添加服务器
|
|
</n-button>
|
|
|
|
<n-button @click="activeRoute = 'chat'">
|
|
<template #icon>
|
|
<n-icon :component="ChatIcon" />
|
|
</template>
|
|
开始对话
|
|
</n-button>
|
|
|
|
<n-button @click="activeRoute = 'tools'">
|
|
<template #icon>
|
|
<n-icon :component="ToolIcon" />
|
|
</template>
|
|
执行工具
|
|
</n-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 最近活动 -->
|
|
<div class="recent-activity">
|
|
<h3>服务器状态</h3>
|
|
<div class="server-list">
|
|
<ServerCard
|
|
v-for="server in servers"
|
|
:key="server.id"
|
|
:server="server"
|
|
@connect="connectServer"
|
|
@disconnect="disconnectServer"
|
|
@edit="editServer"
|
|
@delete="deleteServer"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 服务器管理 -->
|
|
<div v-else-if="activeRoute === 'servers'" class="servers-page">
|
|
<div class="page-header">
|
|
<h2>服务器管理</h2>
|
|
<n-button type="primary" @click="showAddServerModal = true">
|
|
<template #icon>
|
|
<n-icon :component="PlusIcon" />
|
|
</template>
|
|
添加服务器
|
|
</n-button>
|
|
</div>
|
|
|
|
<div class="server-grid">
|
|
<ServerCard
|
|
v-for="server in servers"
|
|
:key="server.id"
|
|
:server="server"
|
|
@connect="connectServer"
|
|
@disconnect="disconnectServer"
|
|
@edit="editServer"
|
|
@delete="deleteServer"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 工具执行 -->
|
|
<div v-else-if="activeRoute === 'tools'" class="tools-page">
|
|
<div class="page-header">
|
|
<h2>工具执行</h2>
|
|
</div>
|
|
|
|
<div v-if="availableTools.length === 0" class="empty-state">
|
|
<n-icon :component="ToolIcon" size="48" />
|
|
<p>暂无可用工具</p>
|
|
<p>请先连接 MCP 服务器</p>
|
|
</div>
|
|
|
|
<div v-else class="tools-grid">
|
|
<ToolForm
|
|
v-for="tool in availableTools"
|
|
:key="`${tool.serverId}-${tool.name}`"
|
|
:tool="tool"
|
|
:llm-enabled="llmConfig.enabled"
|
|
@execute="executeTool"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 对话界面 -->
|
|
<div v-else-if="activeRoute === 'chat'" class="chat-page">
|
|
<div class="chat-container">
|
|
<!-- 聊天消息 -->
|
|
<div class="messages">
|
|
<div
|
|
v-for="message in messages"
|
|
:key="message.id"
|
|
class="message"
|
|
:class="message.role"
|
|
>
|
|
<div class="message-content">
|
|
{{ message.content }}
|
|
</div>
|
|
<div v-if="message.toolCalls" class="tool-calls">
|
|
<div
|
|
v-for="call in message.toolCalls"
|
|
:key="call.id"
|
|
class="tool-call"
|
|
:class="call.status"
|
|
>
|
|
<strong>{{ call.toolName }}</strong>
|
|
<pre>{{ JSON.stringify(call.result, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 输入区域 -->
|
|
<div class="chat-input">
|
|
<n-input
|
|
v-model:value="chatInput"
|
|
type="textarea"
|
|
placeholder="输入你的消息..."
|
|
:autosize="{ minRows: 2, maxRows: 6 }"
|
|
@keydown.enter.prevent="sendMessage"
|
|
/>
|
|
<n-button
|
|
type="primary"
|
|
:loading="chatLoading"
|
|
@click="sendMessage"
|
|
>
|
|
发送
|
|
</n-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 设置页面 -->
|
|
<div v-else-if="activeRoute === 'settings'" class="settings-page">
|
|
<div class="settings-section">
|
|
<h3>LLM 配置</h3>
|
|
<n-form :model="llmConfig" label-placement="left" label-width="120px">
|
|
<n-form-item label="启用 LLM">
|
|
<n-switch v-model:value="llmConfig.enabled" />
|
|
</n-form-item>
|
|
|
|
<n-form-item label="提供商">
|
|
<n-select
|
|
v-model:value="llmConfig.provider"
|
|
:options="[
|
|
{ label: 'OpenAI', value: 'openai' },
|
|
{ label: 'Claude', value: 'claude' },
|
|
{ label: 'Ollama', value: 'ollama' }
|
|
]"
|
|
/>
|
|
</n-form-item>
|
|
|
|
<n-form-item label="模型">
|
|
<n-input v-model:value="llmConfig.model" />
|
|
</n-form-item>
|
|
|
|
<n-form-item label="API Key">
|
|
<n-input
|
|
v-model:value="llmConfig.apiKey"
|
|
type="password"
|
|
show-password-on="click"
|
|
/>
|
|
</n-form-item>
|
|
|
|
<n-form-item>
|
|
<n-button type="primary" @click="saveLLMConfig">
|
|
保存配置
|
|
</n-button>
|
|
<n-button @click="testLLMConnection">
|
|
测试连接
|
|
</n-button>
|
|
</n-form-item>
|
|
</n-form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- 添加服务器模态框 -->
|
|
<n-modal v-model:show="showAddServerModal">
|
|
<n-card title="添加 MCP 服务器" style="width: 600px">
|
|
<ServerForm @submit="handleAddServer" @cancel="showAddServerModal = false" />
|
|
</n-card>
|
|
</n-modal>
|
|
</div>
|
|
</n-notification-provider>
|
|
</n-dialog-provider>
|
|
</n-message-provider>
|
|
</n-config-provider>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, h } from 'vue'
|
|
import { useServerStore } from './stores/newServer'
|
|
import {
|
|
NConfigProvider,
|
|
NGlobalStyle,
|
|
NMessageProvider,
|
|
NDialogProvider,
|
|
NNotificationProvider,
|
|
NMenu,
|
|
NButton,
|
|
NIcon,
|
|
NModal,
|
|
NCard,
|
|
NForm,
|
|
NFormItem,
|
|
NSelect,
|
|
NInput,
|
|
NSwitch,
|
|
darkTheme
|
|
} from 'naive-ui'
|
|
import {
|
|
Dashboard as DashboardIcon,
|
|
Server as ServerIcon,
|
|
Tool as ToolIcon,
|
|
MessageCircle as ChatIcon,
|
|
Settings as SettingsIcon,
|
|
Wifi as WifiIcon,
|
|
WifiOff as WifiOffIcon,
|
|
Sun as SunIcon,
|
|
Moon as MoonIcon,
|
|
Plus as PlusIcon,
|
|
Plug as PlugIcon,
|
|
Brain as BrainIcon
|
|
} from '@vicons/tabler'
|
|
import ServerCard from './components/ServerCard.vue'
|
|
import ToolForm from './components/ToolForm.vue'
|
|
import ServerForm from './components/ServerForm.vue'
|
|
|
|
// 状态管理
|
|
const serverStore = useServerStore()
|
|
|
|
// 响应式数据
|
|
const activeRoute = ref('dashboard')
|
|
const showAddServerModal = ref(false)
|
|
const isDark = ref(false)
|
|
|
|
// 计算属性
|
|
const theme = computed(() => isDark.value ? darkTheme : null)
|
|
const servers = computed(() => serverStore.servers)
|
|
const connectedServers = computed(() => serverStore.connectedServers)
|
|
const availableTools = computed(() => serverStore.availableTools)
|
|
|
|
// 菜单配置
|
|
const menuOptions = [
|
|
{
|
|
label: '仪表盘',
|
|
key: 'dashboard',
|
|
icon: () => h(NIcon, { component: DashboardIcon })
|
|
},
|
|
{
|
|
label: '服务器',
|
|
key: 'servers',
|
|
icon: () => h(NIcon, { component: ServerIcon })
|
|
},
|
|
{
|
|
label: '工具',
|
|
key: 'tools',
|
|
icon: () => h(NIcon, { component: ToolIcon })
|
|
},
|
|
{
|
|
label: '对话',
|
|
key: 'chat',
|
|
icon: () => h(NIcon, { component: ChatIcon })
|
|
},
|
|
{
|
|
label: '设置',
|
|
key: 'settings',
|
|
icon: () => h(NIcon, { component: SettingsIcon })
|
|
}
|
|
]
|
|
|
|
const currentPageTitle = computed(() => {
|
|
const titleMap: Record<string, string> = {
|
|
dashboard: '仪表盘',
|
|
servers: '服务器管理',
|
|
tools: '工具执行',
|
|
chat: '智能对话',
|
|
settings: '系统设置'
|
|
}
|
|
return titleMap[activeRoute.value] || '仪表盘'
|
|
})
|
|
|
|
// 方法
|
|
const handleMenuSelect = (key: string) => {
|
|
activeRoute.value = key
|
|
}
|
|
|
|
const toggleTheme = () => {
|
|
isDark.value = !isDark.value
|
|
}
|
|
|
|
const connectServer = async (serverId: string) => {
|
|
await serverStore.connectServer(serverId)
|
|
}
|
|
|
|
const disconnectServer = async (serverId: string) => {
|
|
await serverStore.disconnectServer(serverId)
|
|
}
|
|
|
|
const editServer = (serverId: string) => {
|
|
// TODO: 实现编辑服务器功能
|
|
console.log('编辑服务器:', serverId)
|
|
}
|
|
|
|
const deleteServer = async (serverId: string) => {
|
|
await serverStore.removeServer(serverId)
|
|
}
|
|
|
|
const handleAddServer = async (config: any) => {
|
|
await serverStore.addServer(config)
|
|
showAddServerModal.value = false
|
|
}
|
|
|
|
const executeTool = async (payload: any) => {
|
|
// 通过 API 执行工具
|
|
console.log('执行工具:', payload)
|
|
}
|
|
|
|
// 生命周期
|
|
onMounted(() => {
|
|
// 应用启动时自动加载本地保存的服务器配置
|
|
console.log('🚀 MCP Vue 客户端启动')
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.app {
|
|
display: flex;
|
|
height: 100vh;
|
|
background: var(--body-color);
|
|
}
|
|
|
|
.sidebar {
|
|
width: 240px;
|
|
background: var(--card-color);
|
|
border-right: 1px solid var(--border-color);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.logo {
|
|
padding: 20px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.logo h2 {
|
|
margin: 0;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.nav {
|
|
flex: 1;
|
|
padding: 16px;
|
|
}
|
|
|
|
.main-content {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header {
|
|
height: 60px;
|
|
padding: 0 20px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
background: var(--card-color);
|
|
}
|
|
|
|
.header-left h1 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.connection-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.page-content {
|
|
flex: 1;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.dashboard {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--card-color);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 8px;
|
|
background: var(--primary-color-pressed);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
}
|
|
|
|
.stat-icon.connected {
|
|
background: var(--success-color);
|
|
}
|
|
|
|
.stat-icon.tools {
|
|
background: var(--info-color);
|
|
}
|
|
|
|
.stat-icon.llm {
|
|
background: var(--warning-color);
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 14px;
|
|
color: var(--text-color-3);
|
|
}
|
|
|
|
.quick-actions {
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.quick-actions h3 {
|
|
margin-bottom: 16px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.recent-activity h3 {
|
|
margin-bottom: 16px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.server-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.page-header h2 {
|
|
margin: 0;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.server-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.tools-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: var(--text-color-3);
|
|
}
|
|
|
|
.empty-state p {
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.chat-container {
|
|
height: calc(100vh - 140px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px 0;
|
|
}
|
|
|
|
.message {
|
|
margin-bottom: 16px;
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
max-width: 80%;
|
|
}
|
|
|
|
.message.user {
|
|
background: var(--primary-color);
|
|
color: white;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.message.assistant {
|
|
background: var(--card-color);
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.chat-input {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: end;
|
|
}
|
|
|
|
.settings-section {
|
|
max-width: 600px;
|
|
background: var(--card-color);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
}
|
|
|
|
.settings-section h3 {
|
|
margin: 0 0 20px 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
</style> |