first commit

This commit is contained in:
douboer
2025-10-14 14:18:20 +08:00
commit d93bc02772
66 changed files with 21393 additions and 0 deletions

679
web/src/App.vue Normal file
View 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>