feat: 更新 Tools 页面

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 14:45:56 +08:00
parent 298ff7c79d
commit 11e26601be

View File

@@ -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.:&#10;-y&#10;@modelcontextprotocol/server-filesystem&#10;/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.:&#10;API_KEY=xxx&#10;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>