2026-03-03 11:49:50 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="extensions-view">
|
|
|
|
|
<div class="extensions-header">
|
|
|
|
|
<a-tabs v-model:activeKey="activeTab" class="extensions-tabs">
|
2026-03-03 23:44:57 +08:00
|
|
|
<a-tab-pane key="tools" tab="工具" />
|
2026-03-05 02:29:01 +08:00
|
|
|
<a-tab-pane key="skills" tab="Skills 管理" />
|
2026-03-03 11:49:50 +08:00
|
|
|
<a-tab-pane key="mcp" tab="MCP 服务器" />
|
|
|
|
|
</a-tabs>
|
|
|
|
|
<div class="header-actions">
|
|
|
|
|
<!-- Skills Tab 的按钮 -->
|
|
|
|
|
<template v-if="activeTab === 'skills'">
|
|
|
|
|
<a-upload
|
|
|
|
|
accept=".zip"
|
|
|
|
|
:show-upload-list="false"
|
|
|
|
|
:custom-request="handleImportUpload"
|
|
|
|
|
:disabled="skillsLoading || skillsImporting"
|
|
|
|
|
>
|
2026-03-04 04:13:05 +08:00
|
|
|
<a-button type="primary" :loading="skillsImporting" class="lucide-icon-btn">
|
2026-03-03 11:49:50 +08:00
|
|
|
<Upload :size="14" />
|
|
|
|
|
<span>导入 ZIP</span>
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-upload>
|
|
|
|
|
<a-button @click="handleSkillsRefresh" :disabled="skillsLoading" class="lucide-icon-btn">
|
|
|
|
|
<RotateCw :size="14" />
|
|
|
|
|
<span>刷新</span>
|
|
|
|
|
</a-button>
|
|
|
|
|
</template>
|
2026-03-03 23:44:57 +08:00
|
|
|
<!-- Tools Tab 的按钮 -->
|
|
|
|
|
<template v-else-if="activeTab === 'tools'">
|
|
|
|
|
<a-button @click="handleToolsRefresh" :disabled="toolsLoading" class="lucide-icon-btn">
|
|
|
|
|
<RotateCw :size="14" />
|
|
|
|
|
<span>刷新</span>
|
|
|
|
|
</a-button>
|
|
|
|
|
</template>
|
2026-03-03 11:49:50 +08:00
|
|
|
<!-- MCP Tab 的按钮 -->
|
|
|
|
|
<template v-else-if="activeTab === 'mcp'">
|
|
|
|
|
<a-button type="primary" @click="handleMcpAdd" class="lucide-icon-btn">
|
|
|
|
|
<Plus :size="14" />
|
|
|
|
|
<span>添加服务器</span>
|
|
|
|
|
</a-button>
|
2026-03-04 04:13:05 +08:00
|
|
|
<a-button @click="handleMcpRefresh" :disabled="mcpLoading" class="lucide-icon-btn">
|
2026-03-03 11:49:50 +08:00
|
|
|
<RotateCw :size="14" />
|
|
|
|
|
<span>刷新</span>
|
|
|
|
|
</a-button>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="extensions-content">
|
2026-03-05 02:29:01 +08:00
|
|
|
<div v-show="activeTab === 'tools'" class="tab-panel">
|
|
|
|
|
<ToolsManagerComponent ref="toolsRef" @refresh="handleToolsRefresh" />
|
|
|
|
|
</div>
|
2026-03-03 11:49:50 +08:00
|
|
|
<div v-show="activeTab === 'skills'" class="tab-panel">
|
|
|
|
|
<SkillsManagerComponent
|
|
|
|
|
ref="skillsRef"
|
|
|
|
|
@import="handleSkillsImport"
|
|
|
|
|
@refresh="handleSkillsRefresh"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-show="activeTab === 'mcp'" class="tab-panel">
|
2026-03-04 04:13:05 +08:00
|
|
|
<McpServersComponent ref="mcpRef" @add="handleMcpAdd" @refresh="handleMcpRefresh" />
|
2026-03-03 11:49:50 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref } from 'vue'
|
|
|
|
|
import { Upload, RotateCw, Plus } from 'lucide-vue-next'
|
|
|
|
|
import SkillsManagerComponent from '@/components/SkillsManagerComponent.vue'
|
2026-03-03 23:44:57 +08:00
|
|
|
import ToolsManagerComponent from '@/components/ToolsManagerComponent.vue'
|
2026-03-03 11:49:50 +08:00
|
|
|
import McpServersComponent from '@/components/McpServersComponent.vue'
|
|
|
|
|
|
2026-03-05 02:29:01 +08:00
|
|
|
const activeTab = ref('tools')
|
2026-03-03 11:49:50 +08:00
|
|
|
const skillsRef = ref(null)
|
2026-03-03 23:44:57 +08:00
|
|
|
const toolsRef = ref(null)
|
2026-03-03 11:49:50 +08:00
|
|
|
const mcpRef = ref(null)
|
|
|
|
|
|
|
|
|
|
// Skills 相关状态(从子组件透传)
|
|
|
|
|
const skillsLoading = ref(false)
|
|
|
|
|
const skillsImporting = ref(false)
|
2026-03-03 23:44:57 +08:00
|
|
|
const toolsLoading = ref(false)
|
2026-03-03 11:49:50 +08:00
|
|
|
const mcpLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
// 暴露给子组件的状态更新
|
|
|
|
|
const updateSkillsState = (loading, importing) => {
|
|
|
|
|
skillsLoading.value = loading
|
|
|
|
|
skillsImporting.value = importing
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 23:44:57 +08:00
|
|
|
const updateToolsState = (loading) => {
|
|
|
|
|
toolsLoading.value = loading
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:49:50 +08:00
|
|
|
const updateMcpState = (loading) => {
|
|
|
|
|
mcpLoading.value = loading
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skills 事件处理
|
|
|
|
|
const handleSkillsImport = () => {
|
|
|
|
|
// 导入完成后自动刷新
|
|
|
|
|
handleSkillsRefresh()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSkillsRefresh = () => {
|
|
|
|
|
if (skillsRef.value?.fetchSkills) {
|
|
|
|
|
updateSkillsState(true, skillsImporting.value)
|
|
|
|
|
skillsRef.value.fetchSkills().finally(() => {
|
|
|
|
|
updateSkillsState(false, skillsImporting.value)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 23:44:57 +08:00
|
|
|
// Tools 事件处理
|
|
|
|
|
const handleToolsRefresh = () => {
|
|
|
|
|
if (toolsRef.value?.fetchTools) {
|
|
|
|
|
updateToolsState(true)
|
|
|
|
|
toolsRef.value.fetchTools().finally(() => {
|
|
|
|
|
updateToolsState(false)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:49:50 +08:00
|
|
|
// MCP 事件处理
|
|
|
|
|
const handleMcpAdd = () => {
|
|
|
|
|
if (mcpRef.value?.showAddModal) {
|
|
|
|
|
mcpRef.value.showAddModal()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleMcpRefresh = () => {
|
|
|
|
|
if (mcpRef.value?.fetchServers) {
|
|
|
|
|
updateMcpState(true)
|
|
|
|
|
mcpRef.value.fetchServers().finally(() => {
|
|
|
|
|
updateMcpState(false)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理导入上传
|
|
|
|
|
const handleImportUpload = async ({ file, onSuccess, onError }) => {
|
|
|
|
|
if (skillsRef.value?.handleImportUpload) {
|
|
|
|
|
updateSkillsState(skillsLoading.value, true)
|
|
|
|
|
try {
|
|
|
|
|
await skillsRef.value.handleImportUpload({ file, onSuccess, onError })
|
|
|
|
|
handleSkillsImport()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
onError?.(e)
|
|
|
|
|
} finally {
|
|
|
|
|
updateSkillsState(skillsLoading.value, false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
.extensions-view {
|
|
|
|
|
height: 100%;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
background-color: var(--gray-0);
|
|
|
|
|
|
|
|
|
|
.extensions-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 0 16px;
|
|
|
|
|
border-bottom: 1px solid var(--gray-150);
|
|
|
|
|
background-color: var(--gray-0);
|
|
|
|
|
|
|
|
|
|
.extensions-tabs {
|
|
|
|
|
flex: 1;
|
|
|
|
|
height: auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
:deep(.ant-tabs-nav) {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
|
|
|
|
&::before {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.ant-tabs-nav::after) {
|
|
|
|
|
content: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.ant-tabs-nav-left-bar) {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.ant-tabs-items) {
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.ant-tabs-tab) {
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.ant-tabs-ink-bar) {
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.extensions-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
.tab-panel {
|
|
|
|
|
height: 100%;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|