feat: 更新前端页面

- Agents, Chat, Settings, Skill, Tools
- Account, Plan, Script
- useSkills composable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 14:26:25 +08:00
parent 7791d198f1
commit 03540fb9e9
9 changed files with 742 additions and 556 deletions

View File

@@ -1,42 +1,88 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import {
FileText,
Globe,
Calculator,
Code,
Braces,
Github,
MessageSquare,
Mail,
Database,
Folder,
GitBranch,
Box,
Wrench,
Server,
Terminal,
Search,
Plus,
Pause,
Play,
Edit,
Trash2,
X,
} from 'lucide-vue-next'
import { useTools } from './tools/useTools'
import '@/views/database/database.css'
interface Tool {
id: number
name: string
type: 'built-in' | 'custom' | 'mcp'
description: string
status: 'active' | 'inactive'
icon?: string
provider?: string
config?: object
createdAt: string
// 使用工具 composable
const { tools, toolsLoading, fetchTools, syncTools, deleteTool: deleteToolApi } = useTools()
// 图标组件映射
const iconComponents: Record<string, any> = {
FileText,
Globe,
Calculator,
Code,
Braces,
Github,
MessageSquare,
Mail,
Database,
Folder,
GitBranch,
Box,
Wrench,
Server,
Terminal,
}
// Mock data for tools
const builtInTools = ref<Tool[]>([
{ id: 1, name: 'File Reader', type: 'built-in', description: 'Read files from the filesystem', status: 'active', icon: 'fa-file-lines', createdAt: '2025-01-15' },
{ id: 2, name: 'Web Search', type: 'built-in', description: 'Search the web for information', status: 'active', icon: 'fa-globe', createdAt: '2025-01-15' },
{ id: 3, name: 'Calculator', type: 'built-in', description: 'Perform mathematical calculations', status: 'active', icon: 'fa-calculator', createdAt: '2025-01-15' },
{ id: 4, name: 'Code Executor', type: 'built-in', description: 'Execute code in a sandbox', status: 'active', icon: 'fa-code', createdAt: '2025-01-15' },
{ id: 5, name: 'JSON Parser', type: 'built-in', description: 'Parse and validate JSON data', status: 'active', icon: 'fa-brackets-curly', createdAt: '2025-01-15' },
])
const getIconComponent = (iconName?: string) => {
if (!iconName) return Box
return iconComponents[iconName] || Box
}
const customTools = ref<Tool[]>([
{ id: 101, name: 'GitHub API', type: 'custom', description: 'Interact with GitHub REST API', status: 'active', icon: 'fa-github', provider: 'Custom', createdAt: '2025-03-01' },
{ id: 102, name: 'Slack Notifier', type: 'custom', description: 'Send notifications to Slack', status: 'inactive', icon: 'fa-slack', provider: 'Custom', createdAt: '2025-03-05' },
{ id: 103, name: 'Email Sender', type: 'custom', description: 'Send emails via SMTP', status: 'active', icon: 'fa-envelope', provider: 'Custom', createdAt: '2025-03-08' },
])
interface Tool {
id: string
name: string
description: string
category: string
provider: string
security_level: string
require_approval: boolean
parameters: string
status: string
type: 'built-in' | 'mcp'
createdAt?: string
}
const mcpTools = ref<Tool[]>([
{ id: 201, name: 'Puppeteer', type: 'mcp', description: 'Browser automation via Puppeteer', status: 'active', icon: 'fa-browser', provider: 'MCP Server', createdAt: '2025-02-10' },
{ id: 202, name: 'SQL Database', type: 'mcp', description: 'Execute SQL queries on databases', status: 'active', icon: 'fa-database', provider: 'MCP Server', createdAt: '2025-02-12' },
{ id: 203, name: 'Filesystem', type: 'mcp', description: 'File operations via MCP', status: 'active', icon: 'fa-folder', provider: 'MCP Server', createdAt: '2025-02-15' },
{ id: 204, name: 'Git Operations', type: 'mcp', description: 'Git repository operations', status: 'inactive', icon: 'fa-git-alt', provider: 'MCP Server', createdAt: '2025-02-18' },
])
// 页面加载时获取工具列表
onMounted(async () => {
await fetchTools()
})
const activeTab = ref<'built-in' | 'custom' | 'mcp'>('built-in')
// 按类型分类工具
const builtInTools = computed(() => {
return tools.value.filter(t => t.provider === 'system')
})
const mcpTools = computed(() => {
return tools.value.filter(t => t.provider !== 'system')
})
const activeTab = ref<'built-in' | 'mcp'>('built-in')
const searchQuery = ref('')
const filterStatus = ref('all')
const editingTool = ref<Tool | null>(null)
@@ -50,21 +96,19 @@ const editForm = ref({
// Statistics
const stats = computed(() => ({
total: builtInTools.value.length + customTools.value.length + mcpTools.value.length,
active: [...builtInTools.value, ...customTools.value, ...mcpTools.value].filter(t => t.status === 'active').length,
total: tools.value.length,
active: tools.value.filter(t => t.status === 'active').length,
builtIn: builtInTools.value.length,
custom: customTools.value.length,
mcp: mcpTools.value.length
}))
const currentTools = computed(() => {
let tools: Tool[] = []
let toolsList: Tool[] = []
switch (activeTab.value) {
case 'built-in': tools = builtInTools.value; break
case 'custom': tools = customTools.value; break
case 'mcp': tools = mcpTools.value; break
case 'built-in': toolsList = builtInTools.value as Tool[]; break
case 'mcp': toolsList = mcpTools.value as Tool[]; break
}
return tools.filter(tool => {
return toolsList.filter(tool => {
const matchSearch = tool.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
tool.description.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchStatus = filterStatus.value === 'all' || tool.status === filterStatus.value
@@ -74,64 +118,64 @@ const currentTools = computed(() => {
const tabCounts = computed(() => ({
'built-in': builtInTools.value.length,
'custom': customTools.value.length,
'mcp': mcpTools.value.length,
}))
const openEdit = (tool: Tool) => {
const openEdit = (tool: any) => {
editingTool.value = tool
editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '' }
isEditing.value = true
}
const saveEdit = () => {
if (editingTool.value) {
const targetArray = editingTool.value.type === 'built-in' ? builtInTools.value :
editingTool.value.type === 'custom' ? customTools.value : mcpTools.value
const index = targetArray.findIndex(t => t.id === editingTool.value!.id)
if (index !== -1) targetArray[index] = { ...targetArray[index], ...editForm.value }
}
isEditing.value = false
}
const cancelEdit = () => { isEditing.value = false; editingTool.value = null }
const toggleStatus = (tool: Tool) => {
const targetArray = tool.type === 'built-in' ? builtInTools.value :
tool.type === 'custom' ? customTools.value : mcpTools.value
const found = targetArray.find(t => t.id === tool.id)
if (found) found.status = found.status === 'active' ? 'inactive' : 'active'
const toggleStatus = async (tool: any) => {
const newStatus = tool.status === 'active' ? 'inactive' : 'active'
// TODO: 调用 API 更新状态
}
const deleteTool = (id: number) => {
switch (activeTab.value) {
case 'built-in': builtInTools.value = builtInTools.value.filter(t => t.id !== id); break
case 'custom': customTools.value = customTools.value.filter(t => t.id !== id); break
case 'mcp': mcpTools.value = mcpTools.value.filter(t => t.id !== id); break
const handleDeleteTool = async (id: string) => {
if (confirm('Are you sure you want to delete this tool?')) {
await deleteToolApi(id)
}
}
// 同步工具
const handleSyncTools = async () => {
await syncTools()
}
</script>
<template>
<div class="p-6 min-h-screen">
<!-- 顶部导航 -->
<div class="flex justify-between items-center mb-6">
<div class="flex justify-between items-center mb-6 h-10">
<div class="flex items-center gap-2">
<i class="fa-solid fa-tools text-orange-500"></i>
<Wrench class="w-5 h-5 text-orange-500" />
<span class="font-medium">Tools</span>
</div>
<button class="btn-primary">
<i class="fa-solid fa-plus"></i>
New Tool
</button>
<div class="h-10 flex gap-2">
<button v-if="activeTab === 'built-in'" @click="handleSyncTools" class="btn-secondary">
<i class="fa-solid fa-sync mr-1"></i>
Sync Tools
</button>
<button v-if="activeTab === 'mcp'" class="btn-primary">
<Plus class="w-4 h-4 mr-1" />
Add MCP
</button>
</div>
</div>
<!-- Tab 导航 -->
<div class="flex items-center gap-2 mb-6">
<button
v-for="(label, tab) in { 'built-in': 'Built-in', 'custom': 'Custom', 'mcp': 'MCP Servers' }"
v-for="(label, tab) in { 'built-in': 'Built-in', 'mcp': 'MCP Servers' }"
:key="tab"
@click="activeTab = tab as 'built-in' | 'custom' | 'mcp'"
@click="activeTab = tab as 'built-in' | 'mcp'"
class="px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all"
:class="activeTab === tab
? 'bg-orange-500 text-white'
@@ -147,12 +191,12 @@ const deleteTool = (id: number) => {
<!-- 搜索和筛选 -->
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
v-model="searchQuery"
type="text"
placeholder="Search tools by name or description..."
class="search-input w-full"
class="search-input w-full pl-10"
>
</div>
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
@@ -164,7 +208,11 @@ const deleteTool = (id: number) => {
<!-- Tools 列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<table v-if="currentTools.length > 0" class="w-full">
<!-- Loading -->
<div v-if="toolsLoading" class="py-12 text-center text-gray-500">
<i class="fa-solid fa-spinner fa-spin text-2xl"></i>
</div>
<table v-else-if="currentTools.length > 0" class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Tool Name</th>
@@ -178,8 +226,8 @@ const deleteTool = (id: number) => {
<tr v-for="tool in currentTools" :key="tool.id" class="table-row">
<td class="px-5 py-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="tool.type === 'built-in' ? 'bg-orange-500/20' : tool.type === 'custom' ? 'bg-blue-500/20' : 'bg-emerald-500/20'">
<i :class="['fa-solid', tool.icon || 'fa-cube', 'text-lg', tool.type === 'built-in' ? 'text-orange-400' : tool.type === 'custom' ? 'text-blue-400' : 'text-emerald-400']"></i>
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="tool.type === 'built-in' ? 'bg-orange-500/20' : 'bg-emerald-500/20'">
<component :is="getIconComponent(tool.icon)" class="w-5 h-5" :class="tool.type === 'built-in' ? 'text-orange-400' : 'text-emerald-400'" />
</div>
<div>
<div class="font-medium">{{ tool.name }}</div>
@@ -188,7 +236,7 @@ const deleteTool = (id: number) => {
</div>
</td>
<td class="px-5 py-4 text-center">
<span class="text-gray-400 text-sm">{{ tool.provider || (tool.type === 'built-in' ? 'System' : 'Custom') }}</span>
<span class="text-gray-400 text-sm">{{ tool.provider || (tool.type === 'built-in' ? 'System' : 'MCP Server') }}</span>
</td>
<td class="px-5 py-4 text-center">
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs" :class="tool.status === 'active' ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'">
@@ -196,17 +244,17 @@ const deleteTool = (id: number) => {
<span class="capitalize">{{ tool.status }}</span>
</span>
</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ tool.createdAt }}</td>
<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'">
<i :class="['fa-solid', tool.status === 'active' ? 'fa-pause' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
<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">
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
</button>
<button @click="deleteTool(tool.id)" class="btn-icon" title="Delete">
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
<button @click="handleDeleteTool(tool.id)" class="btn-icon" title="Delete">
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-400" />
</button>
</div>
</td>
@@ -217,10 +265,10 @@ const deleteTool = (id: number) => {
<!-- 空状态 -->
<div v-else class="empty-box">
<div class="empty-icon">
<i class="fa-solid fa-tools"></i>
<Wrench class="w-12 h-12" />
</div>
<p class="empty-text">No tools found</p>
<p class="empty-tip">Click "New Tool" to add a tool</p>
<p class="empty-tip">Click "Add MCP" to add a new MCP server</p>
</div>
</div>
@@ -231,7 +279,7 @@ const deleteTool = (id: number) => {
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Edit Tool</h3>
<button @click="cancelEdit" class="btn-icon">
<i class="fa-solid fa-xmark text-xl"></i>
<X class="w-5 h-5" />
</button>
</div>