first commit
This commit is contained in:
679
web/src/App.vue
Normal file
679
web/src/App.vue
Normal file
@@ -0,0 +1,679 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user