feat(web): 优化工具管理界面,重构搜索和分类筛选功能,提升用户体验
This commit is contained in:
parent
6078518577
commit
ef0f5ca233
@ -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、图片的预览功能
|
||||||
|
- 优化扩展页工具列表筛选区:将“全部分类”筛选收纳为搜索框右侧的紧凑下拉入口,并复用扩展页侧栏工具条样式,避免影响其他管理组件布局
|
||||||
|
|
||||||
<!-- 添加到这里 -->
|
<!-- 添加到这里 -->
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -6,29 +6,33 @@
|
|||||||
<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="search-box">
|
<div class="sidebar-toolbar">
|
||||||
<a-input
|
<div class="search-box">
|
||||||
v-model:value="searchQuery"
|
<a-input
|
||||||
placeholder="搜索工具..."
|
v-model:value="searchQuery"
|
||||||
allow-clear
|
placeholder="搜索工具..."
|
||||||
class="search-input"
|
allow-clear
|
||||||
>
|
class="search-input"
|
||||||
<template #prefix><Search :size="14" class="text-muted" /></template>
|
>
|
||||||
</a-input>
|
<template #prefix><Search :size="14" class="text-muted" /></template>
|
||||||
</div>
|
</a-input>
|
||||||
<!-- 分类筛选 -->
|
</div>
|
||||||
<div class="category-filter">
|
|
||||||
<a-select
|
<a-tooltip :title="`当前分类:${currentCategoryLabel}`">
|
||||||
v-model:value="selectedCategory"
|
<a-dropdown trigger="click">
|
||||||
placeholder="全部分类"
|
<a-button class="sidebar-tool category-trigger" :class="{ active: !!selectedCategory }">
|
||||||
allow-clear
|
<SlidersHorizontal :size="14" />
|
||||||
style="width: 100%"
|
</a-button>
|
||||||
>
|
<template #overlay>
|
||||||
<a-select-option value="">全部分类</a-select-option>
|
<a-menu :selectedKeys="[selectedCategory || 'all']" @click="handleCategorySelect">
|
||||||
<a-select-option v-for="cat in categories" :key="cat" :value="cat">
|
<a-menu-item key="all">全部分类</a-menu-item>
|
||||||
{{ categoryLabels[cat] || cat }}
|
<a-menu-item v-for="cat in categories" :key="cat">
|
||||||
</a-select-option>
|
{{ categoryLabels[cat] || cat }}
|
||||||
</a-select>
|
</a-menu-item>
|
||||||
|
</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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user