From a96324108848857ae089e17b2a9b8f09c31254b1 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Tue, 3 Mar 2026 11:49:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor(web):=20=E9=87=8D=E6=96=B0=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E8=AE=BE=E7=BD=AE=E5=92=8C=E6=8A=80=E8=83=BD=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从SettingsModal.vue中移除了非超级管理员的MCP管理功能。 - 更新了SkillsManagerComponent.vue以增强布局和功能。 - 引入了新的ExtensionsView.vue来管理技能和MCP服务器。 - 更新路由将/skills更改为/extensions。 - 调整了AppLayout.vue以使用新的图标和路径进行扩展管理。 --- web/src/assets/css/extensions.less | 218 +++++ web/src/components/McpServerDetailModal.vue | 660 ------------- web/src/components/McpServersComponent.vue | 886 ++++++++++++------ web/src/components/SettingsModal.vue | 23 - web/src/components/SkillsManagerComponent.vue | 511 ++++------ web/src/layouts/AppLayout.vue | 10 +- web/src/router/index.js | 13 +- web/src/views/ExtensionsView.vue | 205 ++++ 8 files changed, 1176 insertions(+), 1350 deletions(-) create mode 100644 web/src/assets/css/extensions.less delete mode 100644 web/src/components/McpServerDetailModal.vue create mode 100644 web/src/views/ExtensionsView.vue diff --git a/web/src/assets/css/extensions.less b/web/src/assets/css/extensions.less new file mode 100644 index 00000000..a0aa3351 --- /dev/null +++ b/web/src/assets/css/extensions.less @@ -0,0 +1,218 @@ +@border-color: var(--gray-150); +@bg-secondary: var(--gray-10); +@radius: 8px; + +// Root container for extension pages to ensure 100% height +.extension-page-root { + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: var(--gray-0); + position: relative; +} + +.layout-wrapper { + display: flex; + flex: 1; + min-height: 0; + height: 100%; +} + +/* Sidebar List */ +.sidebar-list { + width: 280px; + border-right: 1px solid @border-color; + background-color: @bg-secondary; + display: flex; + flex-direction: column; + flex-shrink: 0; + + .search-box { + padding: 12px 12px 0; + .search-input :deep(.ant-input) { + height: 28px; + } + } + + .list-container { + flex: 1; + overflow-y: auto; + padding: 12px; + display: flex; + flex-direction: column; + gap: 4px; + } +} + +.list-item { + padding: 10px 12px; + border-radius: @radius; + cursor: pointer; + transition: all 0.2s; + border: 1px solid transparent; + + &:hover { background-color: var(--gray-100); } + + &.active { + background-color: var(--gray-0); + border-color: @border-color; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + } + + .item-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 4px; + + .item-icon, .server-icon { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--gray-400); + font-size: 14px; // For emoji icons + } + + .item-name { + flex: 1; + font-size: 14px; + font-weight: 500; + color: var(--gray-700); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + &.active { + .item-icon, .server-icon { color: var(--main-color); } + .item-name { color: var(--gray-900); } + } +} + +/* Main Panel */ +.main-panel { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + background-color: var(--gray-0); + + .unselected-state { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background-color: @bg-secondary; + .hint-box { + text-align: center; + p { margin-top: 12px; color: var(--gray-400); } + } + } + + .panel-top-bar { + padding: 10px 16px 0 16px; + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; + + h2 { + margin: 0; + font-size: 18px; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; + } + } +} + +/* Tabs inside main panel */ +.detail-tabs, .minimal-tabs { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + + :deep(.ant-tabs-nav) { + margin: 0; + padding: 0 16px; + flex-shrink: 0; + border-bottom: 1px solid @border-color; + &::before { border-bottom: none; } + } + + :deep(.ant-tabs-content) { + flex: 1; + min-height: 0; + height: 100%; + overflow: hidden; + } + + :deep(.ant-tabs-tabpane) { + height: 100%; + min-height: 0; + overflow: hidden; + } +} + +.tab-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; +} + +/* Common helpers */ +.text-muted { color: var(--gray-400); } +.empty-text { padding: 40px 0; text-align: center; color: var(--gray-400); } + +// Spin wrapper for full height +.full-height-spin { + height: 100%; + :deep(.ant-spin-nested-loading) { height: 100%; } + :deep(.ant-spin-container) { + height: 100%; + display: flex; + flex-direction: column; + } +} + +/* Loading States */ +.loading-bar-wrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + z-index: 100; + overflow: hidden; + background: transparent; + + .loading-bar { + height: 100%; + background: var(--main-color); + width: 30%; + position: absolute; + animation: loading-bar-anim 1.5s infinite linear; + } +} + +@keyframes loading-bar-anim { + 0% { left: -30%; } + 100% { left: 100%; } +} + +.content-loading { + opacity: 0.6; + pointer-events: none; + transition: opacity 0.2s; +} diff --git a/web/src/components/McpServerDetailModal.vue b/web/src/components/McpServerDetailModal.vue deleted file mode 100644 index fb9fead6..00000000 --- a/web/src/components/McpServerDetailModal.vue +++ /dev/null @@ -1,660 +0,0 @@ - - - - - diff --git a/web/src/components/McpServersComponent.vue b/web/src/components/McpServersComponent.vue index ff237394..95812a2e 100644 --- a/web/src/components/McpServersComponent.vue +++ b/web/src/components/McpServersComponent.vue @@ -1,128 +1,288 @@ @@ -279,14 +433,21 @@ import { ref, reactive, computed, onMounted } from 'vue' import { notification, Modal } from 'ant-design-vue' import { - PlusOutlined, - EditOutlined, - DeleteOutlined, - EyeOutlined, - ApiOutlined -} from '@ant-design/icons-vue' + Search, + Plug, + Zap, + Pencil, + Trash2, + RotateCw, + Info, + Copy, + Settings2, + Wrench, + MessageSquare, + Box +} from 'lucide-vue-next' import { mcpApi } from '@/apis/mcp_api' -import McpServerDetailModal from './McpServerDetailModal.vue' +import { formatFullDateTime } from '@/utils/time' import McpEnvEditor from './McpEnvEditor.vue' // 状态 @@ -295,6 +456,16 @@ const error = ref(null) const servers = ref([]) const toggleLoading = ref(null) const testLoading = ref(null) +const searchQuery = ref('') +const currentServer = ref(null) +const detailTab = ref('general') + +// 工具相关状态 +const tools = ref([]) +const toolsLoading = ref(false) +const toolsError = ref(null) +const toolSearchText = ref('') +const toggleToolLoading = ref(null) // 表单相关 const formModalVisible = ref(false) @@ -317,21 +488,31 @@ const form = reactive({ icon: '' }) -// 详情模态框 -const detailModalVisible = ref(false) -const selectedServer = ref(null) // 计算属性 -const httpCount = computed( - () => servers.value.filter((s) => s.transport === 'streamable_http').length -) -const sseCount = computed(() => servers.value.filter((s) => s.transport === 'sse').length) -const stdioCount = computed(() => servers.value.filter((s) => s.transport === 'stdio').length) -const envEditorKey = computed(() => `${form.name}-${form.transport}`) +const filteredServers = computed(() => { + if (!searchQuery.value) return servers.value + const q = searchQuery.value.toLowerCase() + return servers.value.filter( + (s) => s.name.toLowerCase().includes(q) || (s.description || '').toLowerCase().includes(q) + ) +}) + const isStdioTransport = computed( () => String(form.transport || '').trim().toLowerCase() === 'stdio' ) +// 工具相关计算属性 +const filteredTools = computed(() => { + if (!toolSearchText.value) return tools.value + const search = toolSearchText.value.toLowerCase() + return tools.value.filter( + (t) => + t.name.toLowerCase().includes(search) || + (t.description && t.description.toLowerCase().includes(search)) + ) +}) + // 获取服务器列表 const fetchServers = async () => { try { @@ -340,6 +521,15 @@ const fetchServers = async () => { const result = await mcpApi.getMcpServers() if (result.success) { servers.value = result.data || [] + // 默认选中第一个服务器 + if (!currentServer.value && servers.value.length > 0) { + selectServer(servers.value[0]) + } else if (currentServer.value) { + const latest = servers.value.find((s) => s.name === currentServer.value.name) + if (latest) { + currentServer.value = latest + } + } } else { error.value = result.message || '获取服务器列表失败' } @@ -351,6 +541,83 @@ const fetchServers = async () => { } } +// 获取工具列表 +const fetchTools = async () => { + if (!currentServer.value) return + + try { + toolsLoading.value = true + toolsError.value = null + const result = await mcpApi.getMcpServerTools(currentServer.value.name) + if (result.success) { + tools.value = result.data || [] + } else { + toolsError.value = result.message || '获取工具列表失败' + tools.value = [] + } + } catch (err) { + console.error('获取工具列表失败:', err) + toolsError.value = err.message || '获取工具列表失败' + tools.value = [] + } finally { + toolsLoading.value = false + } +} + +// 切换工具启用状态 +const handleToggleTool = async (tool) => { + if (!currentServer.value) return + + try { + toggleToolLoading.value = tool.name + const result = await mcpApi.toggleMcpServerTool(currentServer.value.name, tool.name) + if (result.success) { + notification.success({ message: result.message }) + const targetTool = tools.value.find((t) => t.name === tool.name) + if (targetTool) { + targetTool.enabled = result.enabled + } + } else { + notification.error({ message: result.message || '操作失败' }) + } + } catch (err) { + console.error('切换工具状态失败:', err) + notification.error({ message: err.message || '操作失败' }) + } finally { + toggleToolLoading.value = null + } +} + +// 复制工具名称 +const copyToolName = async (name) => { + try { + await navigator.clipboard.writeText(name) + notification.success({ message: '已复制到剪贴板' }) + } catch { + notification.error({ message: '复制失败' }) + } +} + +// 格式化时间 +const formatTime = (timeStr) => formatFullDateTime(timeStr) + +// 获取传输类型颜色 +const getTransportColor = (transport) => { + const colors = { + sse: 'orange', + stdio: 'green', + streamable_http: 'blue' + } + return colors[transport] || 'blue' +} + +// 选择服务器 +const selectServer = (server) => { + currentServer.value = server + detailTab.value = 'general' + fetchTools() +} + // 显示添加模态框 const showAddModal = () => { editMode.value = false @@ -407,12 +674,6 @@ const showEditModal = async (server) => { applyServerToForm(server) } -// 显示详情模态框 -const showDetailModal = (server) => { - selectedServer.value = server - detailModalVisible.value = true -} - // 处理表单提交 const handleFormSubmit = async () => { try { @@ -577,11 +838,6 @@ const confirmDeleteServer = (server) => { }) } -// 处理服务器更新(来自详情模态框) -const handleServerUpdate = () => { - fetchServers() -} - // 格式化 JSON const formatJson = () => { try { @@ -621,193 +877,207 @@ const parseJsonToForm = () => { onMounted(() => { fetchServers() }) + +// 暴露方法给父组件 +defineExpose({ + fetchServers, + showAddModal +}) diff --git a/web/src/components/SettingsModal.vue b/web/src/components/SettingsModal.vue index f82c1dda..64405bf5 100644 --- a/web/src/components/SettingsModal.vue +++ b/web/src/components/SettingsModal.vue @@ -49,15 +49,6 @@ 部门管理 -
- - MCP 管理 -
@@ -86,14 +77,6 @@ > 用户管理 - -
- -
-
@@ -139,13 +118,11 @@ import { SettingOutlined, CodeOutlined, UserOutlined, - ApiOutlined, TeamOutlined } from '@ant-design/icons-vue' import BasicSettingsSection from '@/components/BasicSettingsSection.vue' import ModelProvidersComponent from '@/components/ModelProvidersComponent.vue' import UserManagementComponent from '@/components/UserManagementComponent.vue' -import McpServersComponent from '@/components/McpServersComponent.vue' import DepartmentManagementComponent from '@/components/DepartmentManagementComponent.vue' const props = defineProps({ diff --git a/web/src/components/SkillsManagerComponent.vue b/web/src/components/SkillsManagerComponent.vue index 84e6786b..8d11c46d 100644 --- a/web/src/components/SkillsManagerComponent.vue +++ b/web/src/components/SkillsManagerComponent.vue @@ -1,186 +1,163 @@