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

View File

@ -30,6 +30,20 @@
flex-direction: column; flex-direction: column;
flex-shrink: 0; 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 { .search-box {
padding: 8px 12px 0; 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 { .list-container {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;

View File

@ -37,13 +37,14 @@
<span class="item-name">{{ skill.name }}</span> <span class="item-name">{{ skill.name }}</span>
</div> </div>
<div class="item-status"> <div class="item-status">
<a-button <button
v-if="skill.status === 'update_available'" v-if="skill.status === 'update_available'"
size="small" type="button"
class="skill-inline-action skill-inline-action-secondary"
@click.stop="handleUpdateBuiltin(skill)" @click.stop="handleUpdateBuiltin(skill)"
> >
更新 更新
</a-button> </button>
<span <span
v-else-if="skill.statusLabel" v-else-if="skill.statusLabel"
class="status-chip" class="status-chip"
@ -92,9 +93,13 @@
<span class="item-name">{{ skill.name }}</span> <span class="item-name">{{ skill.name }}</span>
</div> </div>
<div class="item-status"> <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> </div>
<div class="item-details item-details-inline"> <div class="item-details item-details-inline">
@ -144,43 +149,40 @@
> >
{{ currentSkillStatusLabel }} {{ currentSkillStatusLabel }}
</span> </span>
<a-button <button
v-if="currentSkill.is_builtin_spec && currentSkill.status === 'not_installed'" v-if="currentSkill.is_builtin_spec && currentSkill.status === 'not_installed'"
type="primary" type="button"
size="small"
@click="handleInstallBuiltin(currentSkill)" @click="handleInstallBuiltin(currentSkill)"
class="lucide-icon-btn" class="lucide-icon-btn skill-panel-action skill-panel-action-primary"
> >
<span>安装</span> <span>安装</span>
</a-button> </button>
<a-button <button
v-if="currentSkill.is_builtin_spec && currentSkill.status === 'update_available'" v-if="currentSkill.is_builtin_spec && currentSkill.status === 'update_available'"
size="small" type="button"
@click="handleUpdateBuiltin(currentSkill)" @click="handleUpdateBuiltin(currentSkill)"
class="lucide-icon-btn" class="lucide-icon-btn skill-panel-action skill-panel-action-secondary"
> >
<span>更新</span> <span>更新</span>
</a-button> </button>
<a-button <button
v-if="isInstalledSkill" v-if="isInstalledSkill"
size="small" type="button"
@click="handleExport" @click="handleExport"
class="lucide-icon-btn" class="lucide-icon-btn skill-panel-action skill-panel-action-secondary"
> >
<Download :size="14" /> <Download :size="14" />
<span>导出</span> <span>导出</span>
</a-button> </button>
<a-button <button
v-if="isInstalledSkill && !isBuiltinInstalledSkill" v-if="isInstalledSkill"
size="small" type="button"
danger
ghost
@click="confirmDeleteSkill" @click="confirmDeleteSkill"
class="lucide-icon-btn" class="lucide-icon-btn skill-panel-action skill-panel-action-danger"
> >
<Trash2 :size="14" /> <Trash2 :size="14" />
<span>删除</span> <span>{{ isBuiltinInstalledSkill ? '卸载' : '删除' }}</span>
</a-button> </button>
</a-space> </a-space>
</div> </div>
</div> </div>
@ -476,6 +478,10 @@ const isBuiltinInstalledSkill = computed(() => {
return !!(isInstalledSkill.value && (currentSkill.value?.is_builtin || currentSkill.value?.installed_record)) return !!(isInstalledSkill.value && (currentSkill.value?.is_builtin || currentSkill.value?.installed_record))
}) })
const currentSkillDeleteActionText = computed(() => {
return isBuiltinInstalledSkill.value ? '卸载' : '删除'
})
const currentSkillStatusLabel = computed(() => { const currentSkillStatusLabel = computed(() => {
const skill = currentSkill.value const skill = currentSkill.value
if (!skill) return '' if (!skill) return ''
@ -777,22 +783,26 @@ const handleCreateNode = async () => {
const confirmDeleteSkill = () => { const confirmDeleteSkill = () => {
if (!currentSkill.value || !isInstalledSkill.value) return if (!currentSkill.value || !isInstalledSkill.value) return
const actionText = currentSkillDeleteActionText.value
const detailText = isBuiltinInstalledSkill.value
? '卸载后会移除已安装文件和数据库记录,但仍可从“未安装 Skills”中重新安装。'
: '删除后无法恢复,所有文件和配置将永久消失。'
Modal.confirm({ Modal.confirm({
title: `彻底删除技能「${currentSkill.value.slug}」?`, title: `确认${actionText}技能「${currentSkill.value.slug}」?`,
content: '删除后无法恢复,所有文件和配置将永久消失。', content: detailText,
okText: '确认删除', okText: `确认${actionText}`,
okType: 'danger', okType: 'danger',
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
try { try {
await skillApi.deleteSkill(currentSkill.value.slug) await skillApi.deleteSkill(currentSkill.value.slug)
message.success('已删除') message.success(`${actionText}`)
currentSkill.value = null currentSkill.value = null
treeData.value = [] treeData.value = []
resetFileState() resetFileState()
await fetchSkills() await fetchSkills()
} catch { } 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 { .panel-status-chip {

View File

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