fix: 调整 MCP 未添加状态交互

This commit is contained in:
Wenjie Zhang 2026-06-03 21:50:10 +08:00
parent 27dfacbd62
commit bf7a4821d1
2 changed files with 325 additions and 28 deletions

View File

@ -26,43 +26,126 @@
<template v-else>
<div v-if="filteredEnabledServers.length" class="extension-section-header">已添加</div>
<ExtensionCardGrid>
<ExtensionCardGrid :min-width="360">
<InfoCard
v-for="server in filteredEnabledServers"
:key="server.name"
:key="server.slug"
variant="mini"
:title="formatExtensionCardTitle(server.name)"
:subtitle="server.transport"
:description="server.description || '暂无描述'"
:tags="mcpTags(server)"
:status="{ label: '已添加', level: 'success' }"
@click="navigateToDetail(server)"
@click="handleCardClick(server)"
>
<template #icon>
<span class="info-card-emoji-icon">{{ server.icon || '🔌' }}</span>
</template>
<template #action>
<button
type="button"
class="mcp-card-action mcp-card-action-danger"
:disabled="isActionLoading(server)"
:aria-label="server.created_by === 'system' ? '移除 MCP' : '删除 MCP'"
@click.stop="handleRemoveServer(server)"
>
<Check :size="15" class="action-icon action-icon-check" />
<Trash2 :size="15" class="action-icon action-icon-trash" />
</button>
</template>
</InfoCard>
</ExtensionCardGrid>
<div v-if="filteredDisabledServers.length" class="extension-section-header">可添加</div>
<ExtensionCardGrid v-if="filteredDisabledServers.length">
<ExtensionCardGrid v-if="filteredDisabledServers.length" :min-width="360">
<InfoCard
v-for="server in filteredDisabledServers"
:key="server.name"
:key="server.slug"
variant="mini"
:title="formatExtensionCardTitle(server.name)"
:subtitle="server.transport"
:description="server.description || '暂无描述'"
:tags="mcpTags(server)"
action-label="添加"
@click="navigateToDetail(server)"
@action-click="handleSetServerEnabled(server, true)"
@click="openBasicInfo(server)"
>
<template #icon>
<span class="info-card-emoji-icon">{{ server.icon || '🔌' }}</span>
</template>
<template #action>
<button
type="button"
class="mcp-card-action"
:disabled="isActionLoading(server)"
aria-label="添加 MCP"
@click.stop="handleSetServerEnabled(server, true)"
>
<Plus :size="15" class="action-icon" />
</button>
</template>
</InfoCard>
</ExtensionCardGrid>
</template>
<a-modal
v-model:open="basicInfoVisible"
class="mcp-basic-info-modal"
:footer="null"
width="560px"
:destroy-on-close="true"
@cancel="closeBasicInfo"
>
<div v-if="previewServer" class="mcp-basic-info-panel">
<div class="mcp-basic-info-header">
<div class="mcp-basic-info-icon">
<span>{{ previewServer.icon || '🔌' }}</span>
</div>
<div class="mcp-basic-info-title-area">
<div class="mcp-basic-info-title">
{{ formatExtensionCardTitle(previewServer.name) }}
</div>
<div class="mcp-basic-info-meta">
<span>{{ previewServer.transport || '未知传输类型' }}</span>
<span v-if="previewServer.created_by === 'system'" class="mcp-basic-info-tag">
内置
</span>
</div>
</div>
</div>
<div class="mcp-basic-info-body">
<div class="mcp-basic-info-row">
<label>描述</label>
<span>{{ previewServer.description || '暂无描述' }}</span>
</div>
<div class="mcp-basic-info-row">
<label>传输类型</label>
<span>{{ previewServer.transport || '-' }}</span>
</div>
<div
v-if="Array.isArray(previewServer.tags) && previewServer.tags.length > 0"
class="mcp-basic-info-row"
>
<label>标签</label>
<span class="mcp-basic-info-tags">
<a-tag v-for="tag in previewServer.tags" :key="tag">{{ tag }}</a-tag>
</span>
</div>
<div class="mcp-basic-info-row">
<label>创建人</label>
<span>{{ previewServer.created_by || '-' }}</span>
</div>
</div>
<div class="mcp-basic-info-footer">
<a-button @click="closeBasicInfo">关闭</a-button>
<a-button
type="primary"
class="lucide-icon-btn"
:loading="isActionLoading(previewServer)"
@click="handleSetServerEnabled(previewServer, true)"
>
<template #icon><Plus :size="14" /></template>
添加
</a-button>
</div>
</div>
</a-modal>
<McpFormModal
v-model:open="formModalVisible"
:edit-mode="false"
@ -74,8 +157,8 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { Plus, RefreshCw } from 'lucide-vue-next'
import { message, Modal } from 'ant-design-vue'
import { Check, Plus, RefreshCw, Trash2 } from 'lucide-vue-next'
import { mcpApi } from '@/apis/mcp_api'
import ExtensionCardGrid from './ExtensionCardGrid.vue'
import InfoCard from '@/components/shared/InfoCard.vue'
@ -89,6 +172,9 @@ const loading = ref(false)
const servers = ref([])
const searchQuery = ref('')
const formModalVisible = ref(false)
const basicInfoVisible = ref(false)
const previewServer = ref(null)
const actionLoadingSlug = ref('')
const filteredServers = computed(() => {
const sorted = [...servers.value].sort((a, b) =>
@ -111,23 +197,30 @@ const filteredDisabledServers = computed(() =>
filteredServers.value.filter((item) => !item.enabled)
)
const getTransportColor = (transport) => {
const colors = { sse: 'orange', stdio: 'green', streamable_http: 'blue' }
return colors[transport] || 'blue'
}
const mcpTags = (server) => {
const tags = []
if (server.created_by === 'system') tags.push({ name: '内置' })
if (server.transport)
tags.push({ name: server.transport, color: getTransportColor(server.transport) })
return tags
}
const navigateToDetail = (server) => {
router.push({ path: `/extensions/mcp/${encodeURIComponent(server.slug)}` })
}
const handleCardClick = (server) => {
if (server.enabled) {
navigateToDetail(server)
return
}
openBasicInfo(server)
}
const openBasicInfo = (server) => {
previewServer.value = server
basicInfoVisible.value = true
}
const closeBasicInfo = () => {
basicInfoVisible.value = false
previewServer.value = null
}
const isActionLoading = (server) => actionLoadingSlug.value === server?.slug
const handleMcpAdd = () => {
formModalVisible.value = true
}
@ -139,18 +232,56 @@ const handleFormSubmitted = async () => {
const handleSetServerEnabled = async (server, enabled) => {
try {
actionLoadingSlug.value = server.slug
const result = await mcpApi.updateMcpServerStatus(server.slug, enabled)
if (result.success) {
message.success(result.message || `MCP 已${enabled ? '添加' : '移除'}`)
if (enabled) closeBasicInfo()
await fetchServers()
} else {
message.error(result.message || '操作失败')
}
} catch (err) {
message.error(err.message || '操作失败')
} finally {
actionLoadingSlug.value = ''
}
}
const handleRemoveServer = (server) => {
if (server.created_by === 'system') {
handleSetServerEnabled(server, false)
return
}
confirmDeleteServer(server)
}
const confirmDeleteServer = (server) => {
Modal.confirm({
title: '确认删除 MCP',
content: `确定要删除 MCP "${server.name}" 吗?此操作不可撤销。`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
actionLoadingSlug.value = server.slug
const result = await mcpApi.deleteMcpServer(server.slug)
if (result.success) {
message.success('MCP 删除成功')
await fetchServers()
} else {
message.error(result.message || '删除失败')
}
} catch (err) {
message.error(err.message || '删除失败')
} finally {
actionLoadingSlug.value = ''
}
}
})
}
const fetchServers = async () => {
try {
loading.value = true
@ -179,4 +310,164 @@ defineExpose({ fetchServers, loading })
font-size: 18px;
line-height: 1;
}
.mcp-card-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px solid var(--gray-150);
border-radius: 8px;
background: var(--gray-0);
color: var(--main-color);
cursor: pointer;
transition:
border-color 0.18s ease,
background-color 0.18s ease,
color 0.18s ease;
&:hover,
&:focus {
outline: none;
border-color: var(--main-200);
background: var(--main-50);
}
&:disabled {
cursor: not-allowed;
opacity: 0.45;
}
&.mcp-card-action-danger {
color: var(--color-success-700);
.action-icon-trash {
display: none;
}
&:hover,
&:focus {
border-color: var(--color-error-100);
background: var(--color-error-50);
color: var(--color-error-700);
.action-icon-check {
display: none;
}
.action-icon-trash {
display: block;
}
}
}
}
.action-icon {
flex-shrink: 0;
}
.mcp-basic-info-panel {
display: flex;
flex-direction: column;
gap: 14px;
}
.mcp-basic-info-header {
display: flex;
align-items: center;
gap: 10px;
}
.mcp-basic-info-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 9px;
border: 1px solid var(--gray-150);
background: var(--main-50);
color: var(--main-color);
font-size: 18px;
}
.mcp-basic-info-title-area {
min-width: 0;
}
.mcp-basic-info-title {
overflow: hidden;
color: var(--gray-900);
font-size: 16px;
font-weight: 700;
line-height: 22px;
text-overflow: ellipsis;
white-space: nowrap;
}
.mcp-basic-info-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 2px;
color: var(--gray-500);
font-size: 12px;
line-height: 18px;
}
.mcp-basic-info-tag {
display: inline-flex;
align-items: center;
height: 18px;
padding: 0 6px;
border-radius: 999px;
background: var(--gray-100);
color: var(--gray-600);
font-size: 11px;
font-weight: 600;
}
.mcp-basic-info-body {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 16px;
border: 1px solid var(--gray-150);
border-radius: 12px;
background: var(--gray-25);
}
.mcp-basic-info-row {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 12px;
color: var(--gray-700);
font-size: 13px;
line-height: 20px;
label {
color: var(--gray-500);
font-weight: 600;
}
span {
min-width: 0;
overflow-wrap: anywhere;
}
}
.mcp-basic-info-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.mcp-basic-info-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>

View File

@ -673,6 +673,12 @@ const fetchServer = async () => {
loading.value = true
const result = await mcpApi.getMcpServer(slug.value)
if (result.success) {
if (result.data?.enabled === false) {
server.value = null
message.info('请先添加 MCP 后再查看详情')
router.replace({ path: '/extensions', query: { tab: 'mcp' } })
return
}
server.value = result.data
} else {
message.error(result.message || '获取 MCP 详情失败')