feat(web): 优化工具管理界面,重构搜索和分类筛选功能,提升用户体验

This commit is contained in:
Wenjie Zhang 2026-03-28 20:43:58 +08:00
parent 6078518577
commit ef0f5ca233
4 changed files with 235 additions and 60 deletions

View File

@ -45,6 +45,7 @@
- 新增内置Skills reporter并移除内置 Agent reporter数据库报表将由 Skills 完成
- 重构内置 Skills 安装机制:内置 skill 改为在管理页以“未安装”状态展示,支持按需安装、基于 `version + content_hash` 的更新提示与覆盖确认,并对已安装内置 skill 禁止在线文件编辑
- 新增知识库 PDF、图片的预览功能
- 优化扩展页工具列表筛选区:将“全部分类”筛选收纳为搜索框右侧的紧凑下拉入口,并复用扩展页侧栏工具条样式,避免影响其他管理组件布局
<!-- 添加到这里 -->

View File

@ -30,6 +30,20 @@
flex-direction: column;
flex-shrink: 0;
.sidebar-toolbar {
padding: 8px 12px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--gray-150);
.search-box {
flex: 1;
min-width: 0;
padding: 0;
}
}
.search-box {
padding: 8px 12px 0;
@ -76,6 +90,34 @@
// }
}
.sidebar-tool {
width: 28px;
height: 28px;
padding: 0;
border-radius: 8px;
border-color: var(--gray-150);
background-color: var(--gray-0);
color: var(--gray-500);
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: none;
&:hover,
&:focus {
color: var(--gray-700);
border-color: var(--gray-200);
background-color: var(--gray-0);
}
&.active {
color: var(--main-color);
border-color: var(--main-color);
background-color: color-mix(in srgb, var(--main-color) 8%, var(--gray-0));
}
}
.list-container {
flex: 1;
overflow-y: auto;

View File

@ -37,13 +37,14 @@
<span class="item-name">{{ skill.name }}</span>
</div>
<div class="item-status">
<a-button
<button
v-if="skill.status === 'update_available'"
size="small"
type="button"
class="skill-inline-action skill-inline-action-secondary"
@click.stop="handleUpdateBuiltin(skill)"
>
更新
</a-button>
</button>
<span
v-else-if="skill.statusLabel"
class="status-chip"
@ -92,9 +93,13 @@
<span class="item-name">{{ skill.name }}</span>
</div>
<div class="item-status">
<a-button size="small" type="primary" @click.stop="handleInstallBuiltin(skill)">
<button
type="button"
class="skill-inline-action skill-inline-action-primary"
@click.stop="handleInstallBuiltin(skill)"
>
安装
</a-button>
</button>
</div>
</div>
<div class="item-details item-details-inline">
@ -144,43 +149,40 @@
>
{{ currentSkillStatusLabel }}
</span>
<a-button
<button
v-if="currentSkill.is_builtin_spec && currentSkill.status === 'not_installed'"
type="primary"
size="small"
type="button"
@click="handleInstallBuiltin(currentSkill)"
class="lucide-icon-btn"
class="lucide-icon-btn skill-panel-action skill-panel-action-primary"
>
<span>安装</span>
</a-button>
<a-button
</button>
<button
v-if="currentSkill.is_builtin_spec && currentSkill.status === 'update_available'"
size="small"
type="button"
@click="handleUpdateBuiltin(currentSkill)"
class="lucide-icon-btn"
class="lucide-icon-btn skill-panel-action skill-panel-action-secondary"
>
<span>更新</span>
</a-button>
<a-button
</button>
<button
v-if="isInstalledSkill"
size="small"
type="button"
@click="handleExport"
class="lucide-icon-btn"
class="lucide-icon-btn skill-panel-action skill-panel-action-secondary"
>
<Download :size="14" />
<span>导出</span>
</a-button>
<a-button
v-if="isInstalledSkill && !isBuiltinInstalledSkill"
size="small"
danger
ghost
</button>
<button
v-if="isInstalledSkill"
type="button"
@click="confirmDeleteSkill"
class="lucide-icon-btn"
class="lucide-icon-btn skill-panel-action skill-panel-action-danger"
>
<Trash2 :size="14" />
<span>删除</span>
</a-button>
<span>{{ isBuiltinInstalledSkill ? '卸载' : '删除' }}</span>
</button>
</a-space>
</div>
</div>
@ -476,6 +478,10 @@ const isBuiltinInstalledSkill = computed(() => {
return !!(isInstalledSkill.value && (currentSkill.value?.is_builtin || currentSkill.value?.installed_record))
})
const currentSkillDeleteActionText = computed(() => {
return isBuiltinInstalledSkill.value ? '卸载' : '删除'
})
const currentSkillStatusLabel = computed(() => {
const skill = currentSkill.value
if (!skill) return ''
@ -777,22 +783,26 @@ const handleCreateNode = async () => {
const confirmDeleteSkill = () => {
if (!currentSkill.value || !isInstalledSkill.value) return
const actionText = currentSkillDeleteActionText.value
const detailText = isBuiltinInstalledSkill.value
? '卸载后会移除已安装文件和数据库记录,但仍可从“未安装 Skills”中重新安装。'
: '删除后无法恢复,所有文件和配置将永久消失。'
Modal.confirm({
title: `彻底删除技能「${currentSkill.value.slug}」?`,
content: '删除后无法恢复,所有文件和配置将永久消失。',
okText: '确认删除',
title: `确认${actionText}技能「${currentSkill.value.slug}」?`,
content: detailText,
okText: `确认${actionText}`,
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await skillApi.deleteSkill(currentSkill.value.slug)
message.success('已删除')
message.success(`${actionText}`)
currentSkill.value = null
treeData.value = []
resetFileState()
await fetchSkills()
} catch {
message.error('删除失败')
message.error(`${actionText}失败`)
}
}
})
@ -975,6 +985,121 @@ defineExpose({
}
}
}
}
.skill-inline-action {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
min-width: 52px;
height: 24px;
padding: 0 9px;
border-style: solid;
border-width: 1px;
box-shadow: none;
font-size: 12px;
font-weight: 600;
line-height: 1;
transition:
background-color 0.18s ease,
border-color 0.18s ease,
color 0.18s ease;
cursor: pointer;
appearance: none;
&.skill-inline-action-primary {
border-color: transparent;
background: var(--main-600);
color: var(--main-0);
}
&.skill-inline-action-secondary {
border-color: var(--main-100);
background: var(--main-30);
color: var(--main-700);
}
&:hover,
&:focus {
outline: none;
}
&.skill-inline-action-primary:hover,
&.skill-inline-action-primary:focus {
background: var(--main-700);
color: var(--main-0);
}
&.skill-inline-action-secondary:hover,
&.skill-inline-action-secondary:focus {
border-color: var(--main-200);
background: var(--main-50);
color: var(--main-800);
}
}
.skill-panel-action {
min-height: 30px;
padding: 0 12px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
border-style: solid;
border-width: 1px;
box-shadow: none;
font-weight: 500;
transition:
background-color 0.18s ease,
border-color 0.18s ease,
color 0.18s ease;
cursor: pointer;
appearance: none;
&:hover,
&:focus {
outline: none;
}
&.skill-panel-action-primary {
border-color: transparent;
background: var(--main-600);
color: var(--main-0);
}
&.skill-panel-action-primary:hover,
&.skill-panel-action-primary:focus {
background: var(--main-700);
color: var(--main-0);
}
&.skill-panel-action-secondary {
border-color: var(--gray-200);
background: var(--gray-25);
color: var(--gray-700);
&:hover,
&:focus {
border-color: var(--gray-300);
color: var(--gray-900);
background: var(--gray-0);
}
}
&.skill-panel-action-danger {
border-color: var(--color-error-100);
background: var(--color-error-50);
color: var(--color-error-700);
&:hover,
&:focus {
border-color: var(--color-error-100);
background: var(--color-error-100);
color: var(--color-error-900);
}
}
}
.panel-status-chip {

View File

@ -6,29 +6,33 @@
<div class="layout-wrapper" :class="{ 'content-loading': loading }">
<!-- 左侧工具列表 -->
<div class="sidebar-list">
<div class="search-box">
<a-input
v-model:value="searchQuery"
placeholder="搜索工具..."
allow-clear
class="search-input"
>
<template #prefix><Search :size="14" class="text-muted" /></template>
</a-input>
</div>
<!-- 分类筛选 -->
<div class="category-filter">
<a-select
v-model:value="selectedCategory"
placeholder="全部分类"
allow-clear
style="width: 100%"
>
<a-select-option value="">全部分类</a-select-option>
<a-select-option v-for="cat in categories" :key="cat" :value="cat">
{{ categoryLabels[cat] || cat }}
</a-select-option>
</a-select>
<div class="sidebar-toolbar">
<div class="search-box">
<a-input
v-model:value="searchQuery"
placeholder="搜索工具..."
allow-clear
class="search-input"
>
<template #prefix><Search :size="14" class="text-muted" /></template>
</a-input>
</div>
<a-tooltip :title="`当前分类:${currentCategoryLabel}`">
<a-dropdown trigger="click">
<a-button class="sidebar-tool category-trigger" :class="{ active: !!selectedCategory }">
<SlidersHorizontal :size="14" />
</a-button>
<template #overlay>
<a-menu :selectedKeys="[selectedCategory || 'all']" @click="handleCategorySelect">
<a-menu-item key="all">全部分类</a-menu-item>
<a-menu-item v-for="cat in categories" :key="cat">
{{ categoryLabels[cat] || cat }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</div>
<div class="list-container">
@ -143,7 +147,7 @@
<script setup>
import { computed, onMounted, ref } from 'vue'
import { message } from 'ant-design-vue'
import { Search, Wrench, Tag, Tags, FileText, List } from 'lucide-vue-next'
import { Search, Wrench, Tag, Tags, FileText, List, SlidersHorizontal } from 'lucide-vue-next'
import { toolApi } from '@/apis/tool_api'
import { getToolIcon } from '@/components/ToolCallingResult/toolRegistry'
@ -189,6 +193,10 @@ const filteredTools = computed(() => {
return result
})
const currentCategoryLabel = computed(
() => categoryLabels[selectedCategory.value] || (selectedCategory.value ? selectedCategory.value : '全部分类')
)
const fetchTools = async () => {
loading.value = true
try {
@ -209,6 +217,10 @@ const selectTool = (record) => {
currentTool.value = record
}
const handleCategorySelect = ({ key }) => {
selectedCategory.value = key === 'all' ? '' : key
}
onMounted(fetchTools)
//
@ -220,11 +232,6 @@ defineExpose({
<style scoped lang="less">
@import '@/assets/css/extensions.less';
.category-filter {
padding: 8px 12px;
border-bottom: 1px solid var(--gray-150);
}
.list-item {
.item-details {
.item-tags {