feat(admin): paginate tools list endpoint and console UI
- GET /tools returns ToolPageView{total,page,pageSize,records}, pageSize=0 keeps full-list mode
- App.vue adds pager below tools table; scope/audit/category pickers use allTools
- add backend pagination tests and front-end pager contract test
This commit is contained in:
@@ -24,8 +24,10 @@ const tokens = ref([])
|
||||
const openClients = ref([])
|
||||
const scopes = ref([])
|
||||
const tools = ref([])
|
||||
const allTools = ref([])
|
||||
const audits = ref([])
|
||||
const toolFilter = reactive({ category: '', kind: '', keyword: '' })
|
||||
const toolPage = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
const auditFilter = reactive({ person: '', tokenPrefix: '', tool: '', result: '', client: '', from: '', to: '' })
|
||||
const tokenDialog = ref(false)
|
||||
const tokenForm = reactive({ id: null, memo: '', scopeIds: [], scopeId: null, expireTime: null })
|
||||
@@ -40,8 +42,8 @@ const activeScope = ref(null)
|
||||
const selectedToolIds = ref([])
|
||||
|
||||
const title = computed(() => menus.find(item => item.id === current.value)?.label || '')
|
||||
const categories = computed(() => [...new Set(tools.value.map(tool => tool.category))])
|
||||
const groupedTools = computed(() => tools.value.reduce((groups, tool) => {
|
||||
const categories = computed(() => [...new Set(allTools.value.map(tool => tool.category))])
|
||||
const groupedTools = computed(() => allTools.value.reduce((groups, tool) => {
|
||||
const category = tool.category || '其他'
|
||||
;(groups[category] ||= []).push(tool)
|
||||
return groups
|
||||
@@ -96,9 +98,9 @@ async function switchMenu(id) {
|
||||
current.value = id
|
||||
if (id === 'tokens') await loadUsers()
|
||||
if (id === 'clients') await loadOpenClients()
|
||||
if (id === 'tools') await loadTools()
|
||||
if (id === 'tools') await Promise.all([loadTools(), loadAllTools()])
|
||||
if (id === 'scopes') await loadScopes()
|
||||
if (id === 'audit') await Promise.all([loadAudit(), api('/tools').then(data => { tools.value = data })])
|
||||
if (id === 'audit') await Promise.all([loadAudit(), loadAllTools()])
|
||||
}
|
||||
|
||||
async function loadUsers(resetPage = false) {
|
||||
@@ -214,8 +216,21 @@ async function rotateClient(client) {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async function loadTools() {
|
||||
tools.value = await api(`/tools${qs(toolFilter)}`)
|
||||
async function loadTools(resetPage = false) {
|
||||
if (resetPage) toolPage.page = 1
|
||||
const data = await api(`/tools${qs({ ...toolFilter, page: toolPage.page, pageSize: toolPage.pageSize })}`)
|
||||
tools.value = data.records
|
||||
toolPage.total = data.total
|
||||
}
|
||||
|
||||
async function loadAllTools() {
|
||||
const data = await api('/tools?pageSize=0')
|
||||
allTools.value = data.records
|
||||
}
|
||||
|
||||
async function changeToolPage(page) {
|
||||
toolPage.page = page
|
||||
await run(loadTools)
|
||||
}
|
||||
|
||||
async function setToolStatus(tool) {
|
||||
@@ -261,7 +276,7 @@ async function deleteScope(scope) {
|
||||
|
||||
async function openScopeTools(scope) {
|
||||
activeScope.value = scope
|
||||
;[selectedToolIds.value, tools.value] = await Promise.all([api(`/scopes/${scope.id}/tools`), api('/tools')])
|
||||
;[selectedToolIds.value] = await Promise.all([api(`/scopes/${scope.id}/tools`), loadAllTools()])
|
||||
scopeToolsDialog.value = true
|
||||
}
|
||||
|
||||
@@ -411,8 +426,8 @@ onMounted(async () => {
|
||||
<div class="toolbar">
|
||||
<el-select v-model="toolFilter.category" clearable placeholder="全部分类" aria-label="工具分类" data-testid="admin-tool-category"><el-option v-for="item in categories" :key="item" :label="item" :value="item" /></el-select>
|
||||
<el-select v-model="toolFilter.kind" clearable placeholder="全部类型" aria-label="工具类型" data-testid="admin-tool-kind"><el-option label="读" value="read" /><el-option label="写" value="write" /></el-select>
|
||||
<el-input v-model="toolFilter.keyword" clearable placeholder="工具名 / 描述" aria-label="工具关键词" data-testid="admin-tool-keyword" @keyup.enter="run(loadTools)" />
|
||||
<el-button type="primary" data-testid="admin-tool-search" @click="run(loadTools)">查询</el-button>
|
||||
<el-input v-model="toolFilter.keyword" clearable placeholder="工具名 / 描述" aria-label="工具关键词" data-testid="admin-tool-keyword" @keyup.enter="run(() => loadTools(true))" />
|
||||
<el-button type="primary" data-testid="admin-tool-search" @click="run(() => loadTools(true))">查询</el-button>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
@@ -427,6 +442,9 @@ onMounted(async () => {
|
||||
<el-table-column label="同步时间" min-width="145"><template #default="{ row }">{{ date(row.modifyTime) }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="90"><template #default="{ row }"><el-button link :type="row.status === 1 ? 'danger' : 'primary'" :data-testid="`admin-tool-status-${row.id}`" @click="setToolStatus(row)">{{ row.status === 1 ? '停用' : '启用' }}</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination class="user-pager" layout="prev, pager, next, total" :total="toolPage.total"
|
||||
:page-size="toolPage.pageSize" :current-page="toolPage.page" small background
|
||||
data-testid="admin-tool-pager" @current-change="changeToolPage" />
|
||||
<p class="hint">“停用”是全局紧急开关,生效后任何 Token 都无法调用该工具。</p>
|
||||
</article>
|
||||
</template>
|
||||
@@ -456,7 +474,7 @@ onMounted(async () => {
|
||||
<div class="toolbar audit-toolbar">
|
||||
<el-input v-model="auditFilter.person" clearable placeholder="姓名 / 手机号" aria-label="人员" data-testid="admin-audit-person" />
|
||||
<el-input v-model="auditFilter.tokenPrefix" clearable placeholder="Token 前缀" aria-label="Token 前缀" data-testid="admin-audit-token" />
|
||||
<el-select v-model="auditFilter.tool" clearable filterable placeholder="全部工具" aria-label="工具名" data-testid="admin-audit-tool"><el-option v-for="item in tools" :key="item.id" :label="item.name" :value="item.name" /></el-select>
|
||||
<el-select v-model="auditFilter.tool" clearable filterable placeholder="全部工具" aria-label="工具名" data-testid="admin-audit-tool"><el-option v-for="item in allTools" :key="item.id" :label="item.name" :value="item.name" /></el-select>
|
||||
<el-select v-model="auditFilter.result" clearable placeholder="全部结果" aria-label="调用结果" data-testid="admin-audit-result"><el-option label="成功" value="success" /><el-option label="拒绝" value="denied" /><el-option label="错误" value="error" /></el-select>
|
||||
<el-select v-model="auditFilter.client" clearable placeholder="全部客户端" aria-label="客户端" data-testid="admin-audit-client"><el-option label="Kimi" value="mcp-kimi" /><el-option label="Claude" value="mcp-claude" /><el-option label="CLI" value="cli" /><el-option label="未知" value="unknown" /></el-select>
|
||||
<el-date-picker v-model="auditFilter.from" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" aria-label="开始日期" data-testid="admin-audit-from" />
|
||||
|
||||
Reference in New Issue
Block a user