109 lines
4.6 KiB
Vue
109 lines
4.6 KiB
Vue
<template>
|
|
<div class="page-container tools">
|
|
<div class="page-header">
|
|
<h1>工具管理</h1>
|
|
<div class="subtitle">{{ list.length }} 个工具</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="loading"><div class="spinner"></div>加载中...</div>
|
|
<div v-else-if="error" class="error-msg">{{ error }} <button @click="fetchData">重试</button></div>
|
|
|
|
<div v-else class="table-container">
|
|
<table class="tools-table">
|
|
<thead>
|
|
<tr>
|
|
<th>名称</th>
|
|
<th>参数</th>
|
|
<th class="action-col">启用</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="tool in list" :key="tool.name">
|
|
<td class="name-col">
|
|
<div class="tool-name">{{ tool.name }}</div>
|
|
<div class="tool-desc">{{ tool.description || '-' }}</div>
|
|
<div class="tool-category">{{ tool.category }}</div>
|
|
</td>
|
|
<td class="params-col">
|
|
<template v-if="tool.parameters && tool.parameters.properties">
|
|
<span v-for="(val, key) in tool.parameters.properties" :key="key" class="param-tag">
|
|
{{ key }}
|
|
</span>
|
|
</template>
|
|
<span v-else>-</span>
|
|
</td>
|
|
<td class="switch-col">
|
|
<label class="switch" @click.prevent="toggleEnabled(tool)">
|
|
<input type="checkbox" :checked="tool.enabled" />
|
|
<span class="slider"></span>
|
|
</label>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { toolsAPI } from '../utils/api.js'
|
|
|
|
const list = ref([])
|
|
const loading = ref(true)
|
|
const error = ref('')
|
|
|
|
const fetchData = async () => {
|
|
loading.value = true
|
|
error.value = ''
|
|
try {
|
|
const res = await toolsAPI.list()
|
|
if (res.success) {
|
|
const data = res.data.categorized
|
|
const all = []
|
|
Object.keys(data || {}).forEach(cat => {
|
|
if (Array.isArray(data[cat])) {
|
|
all.push(...data[cat].map(t => {
|
|
const func = t.function ? t.function : t
|
|
return { name: func.name, description: func.description, parameters: func.parameters, category: cat, enabled: t.enabled !== false }
|
|
}))
|
|
}
|
|
})
|
|
list.value = all
|
|
} else throw new Error(res.message)
|
|
} catch (e) { error.value = e.message }
|
|
finally { loading.value = false }
|
|
}
|
|
|
|
const toggleEnabled = async (tool) => {
|
|
tool.enabled = !tool.enabled
|
|
}
|
|
|
|
onMounted(fetchData)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.table-container { background: var(--bg-primary); border: 1px solid var(--border-light); border-radius: 12px; overflow: hidden; }
|
|
.tools-table { width: 100%; border-collapse: collapse; }
|
|
.tools-table th { text-align: left; padding: 1rem; background: var(--bg-secondary); font-weight: 600; font-size: 0.85rem; color: var(--text-secondary); border-bottom: 1px solid var(--border-light); }
|
|
.tools-table td { padding: 1rem; border-bottom: 1px solid var(--border-light); vertical-align: middle; }
|
|
.tools-table tr:last-child td { border-bottom: none; }
|
|
.tools-table tr:hover td { background: var(--bg-secondary); }
|
|
.tool-name { font-weight: 600; font-size: 0.95rem; }
|
|
.tool-desc { font-size: 0.8rem; color: var(--text-secondary); margin: 0.25rem 0; }
|
|
.tool-category { font-size: 0.7rem; color: var(--text-tertiary); }
|
|
.params-col { width: 180px; }
|
|
.param-tag { display: inline-block; padding: 0.2rem 0.5rem; background: var(--bg-code); border-radius: 4px; font-size: 0.75rem; color: var(--text-secondary); margin: 0.15rem; }
|
|
.switch-col { text-align: center; }
|
|
.switch { position: relative; display: inline-block; width: 44px; height: 24px; cursor: pointer; }
|
|
.switch input { opacity: 0; width: 0; height: 0; }
|
|
.slider { position: absolute; cursor: pointer; inset: 0; background-color: #ccc; transition: 0.3s; border-radius: 24px; }
|
|
.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: 0.3s; border-radius: 50%; }
|
|
input:checked + .slider { background-color: #16a34a; }
|
|
input:checked + .slider:before { transform: translateX(20px); }
|
|
.loading { text-align: center; padding: 4rem; }
|
|
.error-msg { text-align: center; padding: 2rem; color: var(--accent-primary); background: var(--accent-primary-light); border-radius: 12px; }
|
|
.spinner { width: 40px; height: 40px; border: 3px solid var(--border-light); border-top-color: var(--accent-primary); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1rem; }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
</style>
|