764 lines
17 KiB
Vue
764 lines
17 KiB
Vue
<template>
|
||
<n-config-provider :theme="theme" :theme-overrides="themeOverrides">
|
||
<n-global-style />
|
||
<n-message-provider>
|
||
<div class="app">
|
||
<!-- 侧边栏 -->
|
||
<div class="sidebar" :class="{ collapsed: sidebarCollapsed }">
|
||
<div class="sidebar-header">
|
||
<div class="app-logo">
|
||
<n-icon size="24" color="#3b82f6">
|
||
<Robot />
|
||
</n-icon>
|
||
<span v-if="!sidebarCollapsed" class="app-title">MCP Client</span>
|
||
</div>
|
||
<div class="header-actions">
|
||
<n-button
|
||
v-if="!sidebarCollapsed"
|
||
quaternary
|
||
circle
|
||
@click="toggleTheme"
|
||
class="theme-toggle"
|
||
>
|
||
<template #icon>
|
||
<n-icon>
|
||
<Sun v-if="isDark" />
|
||
<Moon v-else />
|
||
</n-icon>
|
||
</template>
|
||
</n-button>
|
||
<n-button
|
||
quaternary
|
||
circle
|
||
@click="toggleSidebar"
|
||
class="collapse-toggle"
|
||
>
|
||
<template #icon>
|
||
<n-icon>
|
||
<Menu2 />
|
||
</n-icon>
|
||
</template>
|
||
</n-button>
|
||
</div>
|
||
</div>
|
||
|
||
<n-scrollbar class="sidebar-content">
|
||
<div class="nav-section">
|
||
<div v-if="!sidebarCollapsed" class="section-title">核心功能</div>
|
||
<div class="nav-items">
|
||
<div
|
||
class="nav-item"
|
||
:class="{ active: currentRoute === 'chat' }"
|
||
@click="currentRoute = 'chat'"
|
||
>
|
||
<n-icon size="18">
|
||
<MessageCircle />
|
||
</n-icon>
|
||
<span>聊天对话</span>
|
||
<div class="nav-indicator" v-if="currentRoute === 'chat'"></div>
|
||
</div>
|
||
<div
|
||
class="nav-item"
|
||
:class="{ active: currentRoute === 'tools' }"
|
||
@click="currentRoute = 'tools'"
|
||
>
|
||
<n-icon size="18">
|
||
<Tool />
|
||
</n-icon>
|
||
<span>工具管理</span>
|
||
<div class="nav-indicator" v-if="currentRoute === 'tools'"></div>
|
||
</div>
|
||
<div
|
||
class="nav-item"
|
||
:class="{ active: currentRoute === 'data' }"
|
||
@click="currentRoute = 'data'"
|
||
>
|
||
<n-icon size="18">
|
||
<Database />
|
||
</n-icon>
|
||
<span>数据管理</span>
|
||
<div class="nav-indicator" v-if="currentRoute === 'data'"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<n-divider style="margin: 16px 0;" />
|
||
|
||
<div class="nav-section">
|
||
<div class="section-title">设置</div>
|
||
<div class="nav-items">
|
||
<div
|
||
class="nav-item"
|
||
:class="{ active: currentRoute === 'model-providers' }"
|
||
@click="currentRoute = 'model-providers'"
|
||
>
|
||
<n-icon size="18">
|
||
<Brain />
|
||
</n-icon>
|
||
<span>模型服务</span>
|
||
<div class="nav-indicator" v-if="currentRoute === 'model-providers'"></div>
|
||
</div>
|
||
<div
|
||
class="nav-item"
|
||
:class="{ active: currentRoute === 'display-settings' }"
|
||
@click="currentRoute = 'display-settings'"
|
||
>
|
||
<n-icon size="18">
|
||
<Palette />
|
||
</n-icon>
|
||
<span>显示设置</span>
|
||
<div class="nav-indicator" v-if="currentRoute === 'display-settings'"></div>
|
||
</div>
|
||
<div
|
||
class="nav-item"
|
||
:class="{ active: currentRoute === 'mcp' }"
|
||
@click="currentRoute = 'mcp'"
|
||
>
|
||
<n-icon size="18">
|
||
<Settings />
|
||
</n-icon>
|
||
<span>MCP 设置</span>
|
||
<div class="nav-indicator" v-if="currentRoute === 'mcp'"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</n-scrollbar>
|
||
</div>
|
||
|
||
<!-- 主内容区域 -->
|
||
<div class="main-content">
|
||
<!-- 聊天页面 -->
|
||
<ChatLayout v-if="currentRoute === 'chat'" />
|
||
|
||
<!-- 工具页面 -->
|
||
<ToolsManager v-else-if="currentRoute === 'tools'" />
|
||
|
||
<!-- 数据页面 -->
|
||
<DataManager v-else-if="currentRoute === 'data'" />
|
||
|
||
<!-- 模型服务页面 -->
|
||
<ModelService v-else-if="currentRoute === 'model-providers'" />
|
||
|
||
<!-- 显示设置页面 -->
|
||
<!-- 显示设置页面 -->
|
||
<DisplaySettings v-else-if="currentRoute === 'display-settings'" />
|
||
|
||
<!-- MCP 设置页面 -->
|
||
<MCPSettings v-else-if="currentRoute === 'mcp'" />
|
||
|
||
<!-- 默认首页 -->
|
||
<div v-else class="content-page">
|
||
<div class="welcome-header">
|
||
<div class="welcome-logo">
|
||
<n-icon size="48" color="#3b82f6">
|
||
<Robot />
|
||
</n-icon>
|
||
</div>
|
||
<h1>欢迎使用 MCP Vue Client</h1>
|
||
<p>现代化的模型上下文协议客户端</p>
|
||
</div>
|
||
|
||
<div class="welcome-grid">
|
||
<n-card
|
||
class="welcome-card"
|
||
hoverable
|
||
@click="currentRoute = 'chat'"
|
||
>
|
||
<template #cover>
|
||
<div class="card-icon chat-icon">
|
||
<n-icon size="32">
|
||
<MessageCircle />
|
||
</n-icon>
|
||
</div>
|
||
</template>
|
||
<h3>开始对话</h3>
|
||
<p>与AI模型进行智能对话,体验强大的语言理解能力</p>
|
||
</n-card>
|
||
|
||
<n-card
|
||
class="welcome-card"
|
||
hoverable
|
||
@click="currentRoute = 'tools'"
|
||
>
|
||
<template #cover>
|
||
<div class="card-icon tools-icon">
|
||
<n-icon size="32">
|
||
<Tool />
|
||
</n-icon>
|
||
</div>
|
||
</template>
|
||
<h3>使用工具</h3>
|
||
<p>执行各种MCP工具,扩展AI的能力边界</p>
|
||
</n-card>
|
||
|
||
<n-card
|
||
class="welcome-card"
|
||
hoverable
|
||
@click="currentRoute = 'mcp'"
|
||
>
|
||
<template #cover>
|
||
<div class="card-icon mcp-icon">
|
||
<n-icon size="32">
|
||
<Settings />
|
||
</n-icon>
|
||
</div>
|
||
</template>
|
||
<h3>配置服务</h3>
|
||
<p>管理MCP服务器连接,搭建强大的AI生态</p>
|
||
</n-card>
|
||
</div>
|
||
|
||
<div class="stats-overview">
|
||
<n-card title="系统概览">
|
||
<div class="stats-grid">
|
||
<n-statistic label="已连接服务器" :value="0" />
|
||
<n-statistic label="可用工具" :value="0" />
|
||
<n-statistic label="对话次数" :value="0" />
|
||
<n-statistic label="配置模型" :value="0" />
|
||
</div>
|
||
</n-card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</n-message-provider>
|
||
</n-config-provider>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { darkTheme, type GlobalTheme } from 'naive-ui'
|
||
import {
|
||
MessageCircle,
|
||
Tool,
|
||
Database,
|
||
Cpu as Brain,
|
||
Palette,
|
||
Settings,
|
||
Sun,
|
||
Moon,
|
||
Robot,
|
||
Menu2
|
||
} from '@vicons/tabler'
|
||
import DisplaySettings from './components/DisplaySettings.vue'
|
||
import MCPSettings from './components/MCPSettings.vue'
|
||
import ModelService from './components/ModelService.vue'
|
||
import ToolsManager from './components/ToolsManager.vue'
|
||
import DataManager from './components/DataManager.vue'
|
||
import ChatLayout from './components/Chat/ChatLayout.vue'
|
||
import { useModelStore } from './stores/modelStore'
|
||
|
||
type RouteKey =
|
||
| 'chat'
|
||
| 'tools'
|
||
| 'data'
|
||
| 'model-providers'
|
||
| 'display-settings'
|
||
| 'mcp'
|
||
|
||
// 状态管理
|
||
const modelStore = useModelStore()
|
||
|
||
// 响应式数据
|
||
const currentRoute = ref<RouteKey>('chat')
|
||
const isDark = ref(false)
|
||
const currentThemeColor = ref('#18a058')
|
||
const sidebarCollapsed = ref(false)
|
||
|
||
// 加载主题颜色设置
|
||
const loadThemeColor = () => {
|
||
try {
|
||
const saved = localStorage.getItem('cherry-display-settings')
|
||
if (saved) {
|
||
const settings = JSON.parse(saved)
|
||
currentThemeColor.value = settings.primaryColor || '#18a058'
|
||
}
|
||
} catch (error) {
|
||
console.error('获取主题颜色失败:', error)
|
||
}
|
||
}
|
||
|
||
// 计算属性
|
||
const theme = computed<GlobalTheme | null>(() => {
|
||
return isDark.value ? darkTheme : null
|
||
})
|
||
|
||
const themeOverrides = computed(() => {
|
||
const primaryColor = currentThemeColor.value
|
||
return {
|
||
common: {
|
||
primaryColor: primaryColor,
|
||
primaryColorHover: primaryColor + 'CC',
|
||
primaryColorPressed: primaryColor + '99',
|
||
primaryColorSuppl: primaryColor
|
||
},
|
||
Tooltip: {
|
||
color: primaryColor + 'F2', // 95% 透明度
|
||
textColor: '#ffffff',
|
||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||
borderRadius: '6px',
|
||
padding: '6px 12px'
|
||
}
|
||
}
|
||
})
|
||
|
||
// 方法
|
||
const toggleTheme = () => {
|
||
isDark.value = !isDark.value
|
||
document.documentElement.setAttribute('data-theme', isDark.value ? 'dark' : 'light')
|
||
}
|
||
|
||
const toggleSidebar = () => {
|
||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
// 初始化模型服务状态
|
||
modelStore.initialize()
|
||
|
||
// 加载主题颜色
|
||
loadThemeColor()
|
||
|
||
// 监听主题颜色变化
|
||
window.addEventListener('theme-color-changed', (event: any) => {
|
||
currentThemeColor.value = event.detail
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.app {
|
||
display: flex;
|
||
height: 100vh;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
/* 侧边栏样式 */
|
||
.sidebar {
|
||
width: 280px;
|
||
background: #ffffff;
|
||
border-right: 1px solid #e2e8f0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04);
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.sidebar.collapsed {
|
||
width: 64px;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.sidebar.collapsed .sidebar-header {
|
||
padding: 20px 12px;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.app-logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.app-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
}
|
||
|
||
.theme-toggle {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sidebar-content {
|
||
flex: 1;
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.nav-section {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.section-title {
|
||
padding: 0 24px 12px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
color: #64748b;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.nav-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.nav-item {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 24px;
|
||
margin: 0 12px;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #64748b;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.sidebar.collapsed .nav-item {
|
||
padding: 12px;
|
||
margin: 0 6px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.sidebar.collapsed .nav-item span {
|
||
display: none;
|
||
}
|
||
|
||
.nav-item:hover {
|
||
background: #f1f5f9;
|
||
color: #334155;
|
||
}
|
||
|
||
.nav-item.active {
|
||
background: #eff6ff;
|
||
color: #3b82f6;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.nav-indicator {
|
||
position: absolute;
|
||
right: -12px;
|
||
width: 3px;
|
||
height: 20px;
|
||
background: #3b82f6;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.sidebar.collapsed .nav-indicator {
|
||
right: -6px;
|
||
}
|
||
|
||
/* 主内容区域 */
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-y: auto;
|
||
height: calc(100vh - 0px);
|
||
}
|
||
|
||
.content-panel {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.content-page {
|
||
flex: 1;
|
||
padding: 32px;
|
||
overflow-y: auto;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
margin-bottom: 32px;
|
||
padding-bottom: 24px;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.page-header h1 {
|
||
margin: 0;
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: #1e293b;
|
||
}
|
||
|
||
.page-header p {
|
||
margin: 4px 0 0 0;
|
||
color: #64748b;
|
||
font-size: 16px;
|
||
}
|
||
|
||
/* 内容网格布局 */
|
||
.content-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||
gap: 24px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.providers-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 20px;
|
||
}
|
||
|
||
.settings-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 24px;
|
||
}
|
||
|
||
.mcp-grid {
|
||
display: grid;
|
||
gap: 24px;
|
||
}
|
||
|
||
/* 卡片样式 */
|
||
.feature-card,
|
||
.action-card,
|
||
.tools-card,
|
||
.stats-card,
|
||
.provider-card,
|
||
.settings-card,
|
||
.mcp-card {
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.feature-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
font-size: 14px;
|
||
color: #475569;
|
||
}
|
||
|
||
.setting-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 14px;
|
||
color: #475569;
|
||
}
|
||
|
||
/* 欢迎页面样式 */
|
||
.welcome-header {
|
||
text-align: center;
|
||
margin-bottom: 48px;
|
||
}
|
||
|
||
.welcome-logo {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.welcome-header h1 {
|
||
margin: 0 0 12px 0;
|
||
font-size: 36px;
|
||
font-weight: 700;
|
||
color: #1e293b;
|
||
}
|
||
|
||
.welcome-header p {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
color: #64748b;
|
||
}
|
||
|
||
.welcome-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||
gap: 24px;
|
||
margin-bottom: 48px;
|
||
}
|
||
|
||
.welcome-card {
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
border: 1px solid #e2e8f0;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.welcome-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.card-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 80px;
|
||
border-radius: 12px 12px 0 0;
|
||
}
|
||
|
||
.chat-icon {
|
||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||
color: white;
|
||
}
|
||
|
||
.tools-icon {
|
||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||
color: white;
|
||
}
|
||
|
||
.mcp-icon {
|
||
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
||
color: white;
|
||
}
|
||
|
||
.welcome-card h3 {
|
||
margin: 16px 0 8px 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
}
|
||
|
||
.welcome-card p {
|
||
margin: 0;
|
||
color: #64748b;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.stats-overview {
|
||
margin-top: 32px;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 24px;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.app {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sidebar {
|
||
width: 100%;
|
||
height: auto;
|
||
max-height: 300px;
|
||
}
|
||
|
||
.content-page {
|
||
padding: 20px;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 24px;
|
||
padding-bottom: 16px;
|
||
}
|
||
|
||
.page-header h1 {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.content-grid,
|
||
.providers-grid,
|
||
.settings-grid,
|
||
.welcome-grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 16px;
|
||
}
|
||
|
||
.welcome-header h1 {
|
||
font-size: 28px;
|
||
}
|
||
|
||
.stats-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
/* 深色模式 */
|
||
[data-theme="dark"] .app {
|
||
background: #0f172a;
|
||
}
|
||
|
||
[data-theme="dark"] .sidebar {
|
||
background: #1e293b;
|
||
border-right-color: #334155;
|
||
}
|
||
|
||
[data-theme="dark"] .sidebar-header {
|
||
border-bottom-color: #334155;
|
||
}
|
||
|
||
[data-theme="dark"] .app-title {
|
||
color: #f8fafc;
|
||
}
|
||
|
||
[data-theme="dark"] .section-title {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
[data-theme="dark"] .nav-item {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
[data-theme="dark"] .nav-item:hover {
|
||
background: #334155;
|
||
color: #f1f5f9;
|
||
}
|
||
|
||
[data-theme="dark"] .nav-item.active {
|
||
background: rgba(59, 130, 246, 0.2);
|
||
color: #60a5fa;
|
||
}
|
||
|
||
[data-theme="dark"] .content-page {
|
||
background: #0f172a;
|
||
}
|
||
|
||
[data-theme="dark"] .page-header {
|
||
border-bottom-color: #334155;
|
||
}
|
||
|
||
[data-theme="dark"] .page-header h1 {
|
||
color: #f8fafc;
|
||
}
|
||
|
||
[data-theme="dark"] .page-header p {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
[data-theme="dark"] .welcome-header h1 {
|
||
color: #f8fafc;
|
||
}
|
||
|
||
[data-theme="dark"] .welcome-header p {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
[data-theme="dark"] .welcome-card h3 {
|
||
color: #f8fafc;
|
||
}
|
||
|
||
[data-theme="dark"] .welcome-card p {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
[data-theme="dark"] .feature-item {
|
||
color: #cbd5e1;
|
||
}
|
||
|
||
[data-theme="dark"] .setting-item {
|
||
color: #cbd5e1;
|
||
}
|
||
</style> |