feat: 更新 Tools 页面
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@ import { useTools } from './tools/useTools'
|
||||
import '@/views/database/database.css'
|
||||
|
||||
// 使用工具 composable
|
||||
const { tools, toolsLoading, fetchTools, syncTools, deleteTool: deleteToolApi } = useTools()
|
||||
const { tools, toolsLoading, fetchTools, syncTools, deleteTool: deleteToolApi, createTool } = useTools()
|
||||
|
||||
// 图标组件映射
|
||||
const iconComponents: Record<string, any> = {
|
||||
@@ -65,7 +65,8 @@ interface Tool {
|
||||
parameters: string
|
||||
status: string
|
||||
type: 'built-in' | 'mcp'
|
||||
createdAt?: string
|
||||
icon?: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
// 页面加载时获取工具列表
|
||||
@@ -92,6 +93,7 @@ const editForm = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
provider: '',
|
||||
icon: '',
|
||||
})
|
||||
|
||||
// Statistics
|
||||
@@ -123,7 +125,7 @@ const tabCounts = computed(() => ({
|
||||
|
||||
const openEdit = (tool: any) => {
|
||||
editingTool.value = tool
|
||||
editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '' }
|
||||
editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '', icon: tool.icon || '' }
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
@@ -133,9 +135,32 @@ const saveEdit = () => {
|
||||
|
||||
const cancelEdit = () => { isEditing.value = false; editingTool.value = null }
|
||||
|
||||
const toggleStatus = async (tool: any) => {
|
||||
// 状态切换确认弹窗
|
||||
const showStatusConfirm = ref(false)
|
||||
const toolToToggle = ref<any>(null)
|
||||
|
||||
const confirmToggleStatus = (tool: any) => {
|
||||
toolToToggle.value = tool
|
||||
showStatusConfirm.value = true
|
||||
}
|
||||
|
||||
const toggleStatus = async () => {
|
||||
if (!toolToToggle.value) return
|
||||
|
||||
const tool = toolToToggle.value
|
||||
const newStatus = tool.status === 'active' ? 'inactive' : 'active'
|
||||
const action = newStatus === 'active' ? 'activate' : 'deactivate'
|
||||
|
||||
// TODO: 调用 API 更新状态
|
||||
console.log(`${action} tool:`, tool.name)
|
||||
|
||||
showStatusConfirm.value = false
|
||||
toolToToggle.value = null
|
||||
}
|
||||
|
||||
const cancelToggleStatus = () => {
|
||||
showStatusConfirm.value = false
|
||||
toolToToggle.value = null
|
||||
}
|
||||
|
||||
const handleDeleteTool = async (id: string) => {
|
||||
@@ -148,6 +173,67 @@ const handleDeleteTool = async (id: string) => {
|
||||
const handleSyncTools = async () => {
|
||||
await syncTools()
|
||||
}
|
||||
|
||||
// Add MCP 弹窗
|
||||
const showAddMcp = ref(false)
|
||||
const mcpForm = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
description_cn: '',
|
||||
transport: 'stdio',
|
||||
command: '',
|
||||
args: '',
|
||||
env: '',
|
||||
})
|
||||
|
||||
const openAddMcp = () => {
|
||||
mcpForm.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
description_cn: '',
|
||||
transport: 'stdio',
|
||||
command: '',
|
||||
args: '',
|
||||
env: '',
|
||||
}
|
||||
showAddMcp.value = true
|
||||
}
|
||||
|
||||
const closeAddMcp = () => {
|
||||
showAddMcp.value = false
|
||||
}
|
||||
|
||||
const submitMcp = async () => {
|
||||
try {
|
||||
// 组装参数
|
||||
const argsArray = mcpForm.value.args ? mcpForm.value.args.split('\n').filter((a: string) => a.trim()) : []
|
||||
const envObj: Record<string, string> = {}
|
||||
if (mcpForm.value.env) {
|
||||
mcpForm.value.env.split('\n').forEach((line: string) => {
|
||||
const [key, ...valueParts] = line.split('=')
|
||||
if (key && valueParts.length > 0) {
|
||||
envObj[key.trim()] = valueParts.join('=').trim()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await createTool({
|
||||
name: mcpForm.value.name,
|
||||
description: mcpForm.value.description,
|
||||
description_cn: mcpForm.value.description_cn,
|
||||
category: 'mcp',
|
||||
provider: 'mcp',
|
||||
transport: mcpForm.value.transport,
|
||||
command: mcpForm.value.command,
|
||||
args: JSON.stringify(argsArray),
|
||||
env: JSON.stringify(envObj),
|
||||
status: 'active',
|
||||
})
|
||||
closeAddMcp()
|
||||
} catch (error) {
|
||||
console.error('Failed to add MCP:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -163,7 +249,7 @@ const handleSyncTools = async () => {
|
||||
<i class="fa-solid fa-sync mr-1"></i>
|
||||
Sync Tools
|
||||
</button>
|
||||
<button v-if="activeTab === 'mcp'" class="btn-primary">
|
||||
<button v-if="activeTab === 'mcp'" @click="openAddMcp" class="btn-primary">
|
||||
<Plus class="w-4 h-4 mr-1" />
|
||||
Add MCP
|
||||
</button>
|
||||
@@ -247,15 +333,19 @@ const handleSyncTools = async () => {
|
||||
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ tool.created_at || '-' }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button @click="toggleStatus(tool)" class="btn-icon" :title="tool.status === 'active' ? 'Deactivate' : 'Activate'">
|
||||
<!-- 激活/停用按钮 - 所有工具都有 -->
|
||||
<button @click="confirmToggleStatus(tool)" class="btn-icon" :title="tool.status === 'active' ? 'Deactivate' : 'Activate'">
|
||||
<component :is="tool.status === 'active' ? Pause : Play" class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="openEdit(tool)" class="btn-icon" title="Edit">
|
||||
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="handleDeleteTool(tool.id)" class="btn-icon" title="Delete">
|
||||
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-400" />
|
||||
</button>
|
||||
<!-- 编辑和删除按钮 - 只有 MCP 工具才有 -->
|
||||
<template v-if="tool.provider !== 'system'">
|
||||
<button @click="openEdit(tool)" class="btn-icon" title="Edit">
|
||||
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="handleDeleteTool(tool.id)" class="btn-icon" title="Delete">
|
||||
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-400" />
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -292,6 +382,10 @@ const handleSyncTools = async () => {
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Provider</label>
|
||||
<input v-model="editForm.provider" type="text" class="input-field">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Icon</label>
|
||||
<input v-model="editForm.icon" type="text" placeholder="e.g., FileText, Globe, Code" class="input-field">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea v-model="editForm.description" rows="3" class="input-field resize-none"></textarea>
|
||||
@@ -305,6 +399,107 @@ const handleSyncTools = async () => {
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 状态切换确认弹窗 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="showStatusConfirm" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="cancelToggleStatus">
|
||||
<div class="bg-dark-700 rounded-xl w-full max-w-sm border border-dark-500 shadow-2xl" @click.stop>
|
||||
<div class="p-6 text-center">
|
||||
<div class="w-14 h-14 rounded-full flex items-center justify-center mx-auto mb-4" :class="toolToToggle?.status === 'active' ? 'bg-yellow-500/20' : 'bg-green-500/20'">
|
||||
<component :is="toolToToggle?.status === 'active' ? Pause : Play" class="text-xl" :class="toolToToggle?.status === 'active' ? 'text-yellow-400' : 'text-green-400'" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-white mb-2">
|
||||
{{ toolToToggle?.status === 'active' ? 'Stop Tool' : 'Activate Tool' }}
|
||||
</h3>
|
||||
<p class="text-gray-400 text-sm">
|
||||
{{ toolToToggle?.status === 'active'
|
||||
? `Are you sure you want to stop "${toolToToggle?.name}"? This tool will no longer be available.`
|
||||
: `Are you sure you want to activate "${toolToToggle?.name}"? This tool will become available.`
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex border-t border-dark-500">
|
||||
<button @click="cancelToggleStatus" class="flex-1 py-3 text-gray-400 hover:text-white hover:bg-dark-600 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button @click="toggleStatus" class="flex-1 py-3 transition-colors border-l border-dark-500" :class="toolToToggle?.status === 'active' ? 'text-yellow-400 hover:bg-yellow-500/10' : 'text-green-400 hover:bg-green-500/10'">
|
||||
{{ toolToToggle?.status === 'active' ? 'Stop' : 'Activate' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- Add MCP 弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showAddMcp" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50 modal-overlay" @click="closeAddMcp">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-xl border border-dark-500 shadow-2xl modal-content" @click.stop>
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Add MCP Server</h3>
|
||||
<button @click="closeAddMcp" class="btn-icon">
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4 max-h-[70vh] overflow-y-auto">
|
||||
<!-- MCP Name -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">MCP Name <span class="text-red-400">*</span></label>
|
||||
<input v-model="mcpForm.name" type="text" class="input-field" placeholder="e.g., filesystem, memory">
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description (English)</label>
|
||||
<textarea v-model="mcpForm.description" rows="2" class="input-field resize-none" placeholder="Describe what this MCP server does"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Description CN -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description (中文)</label>
|
||||
<textarea v-model="mcpForm.description_cn" rows="2" class="input-field resize-none" placeholder="描述这个 MCP 服务器的功能"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Transport -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Transport Protocol <span class="text-red-400">*</span></label>
|
||||
<select v-model="mcpForm.transport" class="input-field">
|
||||
<option value="stdio">stdio</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="sse">SSE</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Command -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Start Command <span class="text-red-400">*</span></label>
|
||||
<input v-model="mcpForm.command" type="text" class="input-field" placeholder="e.g., npx, python, node">
|
||||
</div>
|
||||
|
||||
<!-- Args -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Arguments</label>
|
||||
<textarea v-model="mcpForm.args" rows="3" class="input-field resize-none" placeholder="One argument per line, e.g.: -y @modelcontextprotocol/server-filesystem /path/to/dir"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Environment Variables -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Environment Variables</label>
|
||||
<textarea v-model="mcpForm.env" rows="3" class="input-field resize-none" placeholder="KEY=value format, one per line, e.g.: API_KEY=xxx DEBUG=true"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button @click="closeAddMcp" class="btn-secondary">Cancel</button>
|
||||
<button @click="submitMcp" class="btn-primary" :disabled="!mcpForm.name || !mcpForm.command">
|
||||
Add MCP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user