diff --git a/xiaoqu-agent-service/front/src/App.vue b/xiaoqu-agent-service/front/src/App.vue
index f17dbe0..ae9c6ec 100644
--- a/xiaoqu-agent-service/front/src/App.vue
+++ b/xiaoqu-agent-service/front/src/App.vue
@@ -40,6 +40,9 @@ const scopeForm = reactive({ id: null, name: '', code: '', memo: '' })
const scopeToolsDialog = ref(false)
const activeScope = ref(null)
const selectedToolIds = ref([])
+const scopeToolsTab = ref('')
+const scopeToolsKeyword = ref('')
+const scopeToolsOnlySelected = ref(false)
const title = computed(() => menus.find(item => item.id === current.value)?.label || '')
const categories = computed(() => [...new Set(allTools.value.map(tool => tool.category))])
@@ -48,6 +51,27 @@ const groupedTools = computed(() => allTools.value.reduce((groups, tool) => {
;(groups[category] ||= []).push(tool)
return groups
}, {}))
+const scopeToolTabs = computed(() => Object.entries(groupedTools.value).map(([category, items]) => ({
+ category,
+ total: items.length,
+ selected: items.filter(tool => selectedToolIds.value.includes(tool.id)).length,
+})))
+const scopeToolsVisible = computed(() => {
+ const keyword = scopeToolsKeyword.value.trim().toLowerCase()
+ const items = scopeToolsTab.value ? (groupedTools.value[scopeToolsTab.value] || []) : allTools.value
+ return items.filter(tool => {
+ if (scopeToolsOnlySelected.value && !selectedToolIds.value.includes(tool.id)) return false
+ if (!keyword) return true
+ return tool.name.toLowerCase().includes(keyword) || (tool.description || '').toLowerCase().includes(keyword)
+ })
+})
+const scopeToolsSummary = computed(() => {
+ const detail = scopeToolTabs.value.filter(tab => tab.selected).map(tab => `${tab.category} ×${tab.selected}`).join(' · ')
+ return { selected: selectedToolIds.value.length, total: allTools.value.length, detail }
+})
+const scopeToolsAllVisibleSelected = computed(() => scopeToolsVisible.value.length > 0
+ && scopeToolsVisible.value.every(tool => selectedToolIds.value.includes(tool.id)))
+const scopeToolsSomeVisibleSelected = computed(() => scopeToolsVisible.value.some(tool => selectedToolIds.value.includes(tool.id)))
const issuedKeyCurl = computed(() => {
if (!issuedOpenKey.value) return ''
const payload = JSON.stringify({
@@ -276,10 +300,23 @@ async function deleteScope(scope) {
async function openScopeTools(scope) {
activeScope.value = scope
+ scopeToolsTab.value = ''; scopeToolsKeyword.value = ''; scopeToolsOnlySelected.value = false
;[selectedToolIds.value] = await Promise.all([api(`/scopes/${scope.id}/tools`), loadAllTools()])
scopeToolsDialog.value = true
}
+function toggleScopeTool(id, checked) {
+ const set = new Set(selectedToolIds.value)
+ if (checked) set.add(id); else set.delete(id)
+ selectedToolIds.value = [...set]
+}
+
+function toggleScopeToolsVisible(checked) {
+ const set = new Set(selectedToolIds.value)
+ scopeToolsVisible.value.forEach(tool => { if (checked) set.add(tool.id); else set.delete(tool.id) })
+ selectedToolIds.value = [...set]
+}
+
async function saveScopeTools() {
await run(async () => {
await api(`/scopes/${activeScope.value.id}/tools`, { method: 'PUT', body: JSON.stringify({ toolIds: selectedToolIds.value }) })
@@ -548,11 +585,42 @@ onMounted(async () => {
取消保存
-
-
-
-
- 取消保存配置
+
+
+
+ 仅看已选
+
+
+
+ 全部({{ allTools.length }}){{ scopeToolsSummary.selected }}
+
+
+ {{ tab.category }}({{ tab.total }}){{ tab.selected }}
+
+
+
+
+
+
+
+
+ toggleScopeTool(row.id, checked)" />
+
+
+ {{ row.name }}
+
+ {{ row.kind === 'write' ? '写' : '读' }}
+ {{ row.status === 1 ? '启用' : '已停用' }}
+
+
+
+
diff --git a/xiaoqu-agent-service/front/src/style.css b/xiaoqu-agent-service/front/src/style.css
index 0244308..609c252 100644
--- a/xiaoqu-agent-service/front/src/style.css
+++ b/xiaoqu-agent-service/front/src/style.css
@@ -52,13 +52,14 @@ code { font-family: Menlo, Consolas, monospace; font-size: 12px; }
.audit-toolbar .el-input { width: 150px; }
.audit-toolbar .el-select { width: 140px; }
.audit-toolbar .el-date-editor { width: 150px; }
-.tool-groups { display: block; max-height: 520px; overflow: auto; }
-.tool-group { margin-bottom: 12px; border: 1px solid #ebeef5; border-radius: 4px; }
-.tool-group h3 { margin: 0; padding: 9px 12px; font-size: 14px; background: #fafafa; border-bottom: 1px solid #ebeef5; }
-.tool-group .el-checkbox { width: 100%; height: auto; margin: 0; padding: 9px 12px; align-items: flex-start; }
-.tool-group .el-checkbox + .el-checkbox { border-top: 1px solid #f2f3f5; }
-.tool-group .el-checkbox__label { display: flex; flex-direction: column; white-space: normal; }
-.tool-group small { margin-top: 3px; color: #909399; }
+.scope-tools-toolbar { display: flex; gap: 12px; align-items: center; }
+.scope-tools-toolbar .el-input { max-width: 260px; }
+.scope-tools-tabs { margin-top: 4px; }
+.scope-tools-tabs .el-tabs__header { margin-bottom: 8px; }
+.tab-badge { display: inline-block; margin-left: 6px; padding: 0 6px; font-size: 12px; line-height: 16px; border-radius: 8px; background: #f0f9eb; color: #67c23a; vertical-align: 2px; }
+.scope-tools-footer { display: flex; align-items: center; justify-content: space-between; width: 100%; }
+.scope-tools-summary { color: #606266; font-size: 13px; text-align: left; }
+.el-table .row-selected { background: #ecf5ff; }
.token-secret { display: block; margin-top: 14px; padding: 14px; word-break: break-all; border-radius: 4px; background: #f4f4f5; font-size: 13px; }
.issued-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
.issued-row .token-secret { flex: 1; margin-top: 0; padding: 9px 12px; }