feat:聊天tool前端入口
This commit is contained in:
416
Yi.Ai.Vue3/src/components/ToolList/index.vue
Normal file
416
Yi.Ai.Vue3/src/components/ToolList/index.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<script setup lang="ts">
|
||||
import { ChromeFilled, ElementPlus, Loading, MagicStick, Search } from '@element-plus/icons-vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { aiChatTool } from '@/api';
|
||||
|
||||
// API返回的工具接口
|
||||
interface ApiToolItem {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: string;
|
||||
properties: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
// 前端工具按钮的数据
|
||||
interface ToolItem {
|
||||
id: number;
|
||||
name: string;
|
||||
apiName: string;
|
||||
icon: any;
|
||||
tip: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits<{
|
||||
'tools-update': [payload: {
|
||||
selectedToolIds: number[];
|
||||
selectedApiTools: string[];
|
||||
}];
|
||||
}>();
|
||||
|
||||
// 图标映射配置 - 根据API名称映射图标
|
||||
const iconMap: Record<string, any> = {
|
||||
online_search: ChromeFilled, // 在线搜索
|
||||
deep_think: ElementPlus, // 深度思考
|
||||
search: Search, // 搜索
|
||||
web_search: ChromeFilled, // 网页搜索
|
||||
thinking: MagicStick, // 思考
|
||||
default: ElementPlus, // 默认图标
|
||||
};
|
||||
|
||||
// 提示信息映射
|
||||
const tipMap: Record<string, string> = {
|
||||
online_search: '实时搜索最新信息,获取网络资料',
|
||||
deep_think: '深度推理分析,解决复杂问题',
|
||||
web_search: '联网搜索网页内容',
|
||||
thinking: '深入思考和分析问题',
|
||||
default: '点击启用此功能',
|
||||
};
|
||||
|
||||
// 响应式工具列表 - 初始为空,等待接口加载
|
||||
const tools = ref<ToolItem[]>([]);
|
||||
|
||||
// 当前选中的工具ID数组(支持多选)
|
||||
const activeToolIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
// 从API数据转换为前端格式
|
||||
function transformApiTools(apiTools: ApiToolItem[]): ToolItem[] {
|
||||
return apiTools.map((tool, index) => {
|
||||
const apiName = tool.name;
|
||||
return {
|
||||
id: index + 1,
|
||||
name: tool.description, // 使用中文描述
|
||||
apiName,
|
||||
icon: iconMap[apiName] || iconMap.default,
|
||||
tip: tipMap[apiName] || tipMap.default,
|
||||
enabled: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有选中的工具对象
|
||||
const selectedTools = computed(() => {
|
||||
return tools.value.filter(tool => activeToolIds.value.includes(tool.id));
|
||||
});
|
||||
|
||||
// 获取所有选中的API工具名称
|
||||
const selectedApiTools = computed(() => {
|
||||
return selectedTools.value.map(tool => tool.apiName);
|
||||
});
|
||||
|
||||
// 点击事件处理
|
||||
function handleToolClick(tool: ToolItem) {
|
||||
if (!tool.enabled)
|
||||
return;
|
||||
|
||||
const index = activeToolIds.value.indexOf(tool.id);
|
||||
|
||||
if (index > -1) {
|
||||
activeToolIds.value.splice(index, 1);
|
||||
}
|
||||
else {
|
||||
activeToolIds.value.push(tool.id);
|
||||
}
|
||||
|
||||
console.log('当前选中的工具:', selectedTools.value.map(t => t.name));
|
||||
console.log('对应的API名称:', selectedApiTools.value);
|
||||
|
||||
emitToolsUpdate();
|
||||
}
|
||||
|
||||
// 辅助函数:检查某个工具是否被选中
|
||||
function isActive(toolId: number) {
|
||||
return activeToolIds.value.includes(toolId);
|
||||
}
|
||||
|
||||
// 清空所有选中
|
||||
function clearSelection() {
|
||||
activeToolIds.value = [];
|
||||
emitToolsUpdate();
|
||||
}
|
||||
|
||||
// 全选已启用的工具
|
||||
function selectAll() {
|
||||
activeToolIds.value = tools.value
|
||||
.filter(tool => tool.enabled)
|
||||
.map(tool => tool.id);
|
||||
emitToolsUpdate();
|
||||
}
|
||||
|
||||
// 启用/禁用工具
|
||||
function setToolEnabled(toolId: number, enabled: boolean) {
|
||||
const tool = tools.value.find(t => t.id === toolId);
|
||||
if (tool) {
|
||||
tool.enabled = enabled;
|
||||
if (!enabled && isActive(toolId)) {
|
||||
const index = activeToolIds.value.indexOf(toolId);
|
||||
if (index > -1) {
|
||||
activeToolIds.value.splice(index, 1);
|
||||
emitToolsUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据API名称启用/禁用工具
|
||||
function setToolEnabledByApiName(apiName: string, enabled: boolean) {
|
||||
const tool = tools.value.find(t => t.apiName === apiName);
|
||||
if (tool) {
|
||||
setToolEnabled(tool.id, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
// 通知父组件工具状态变化
|
||||
function emitToolsUpdate() {
|
||||
console.log('工具状态已更新:', {
|
||||
selectedToolIds: activeToolIds.value,
|
||||
selectedTools: selectedTools.value,
|
||||
selectedApiTools: selectedApiTools.value,
|
||||
});
|
||||
|
||||
// 触发自定义事件
|
||||
emit('tools-update', {
|
||||
selectedToolIds: activeToolIds.value,
|
||||
selectedApiTools: selectedApiTools.value,
|
||||
});
|
||||
}
|
||||
|
||||
// 从API获取工具列表
|
||||
async function getAiChatToolList() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const res = await aiChatTool();
|
||||
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
console.log('API返回的工具列表:', res.data);
|
||||
|
||||
// 转换API数据
|
||||
const apiTools = transformApiTools(res.data);
|
||||
|
||||
// 更新工具列表
|
||||
tools.value = apiTools;
|
||||
|
||||
// 默认选中第一个可用的工具
|
||||
const firstEnabledTool = apiTools.find(tool => tool.enabled);
|
||||
if (firstEnabledTool && activeToolIds.value.length === 0) {
|
||||
activeToolIds.value = [firstEnabledTool.id];
|
||||
}
|
||||
|
||||
console.log('加载的工具列表:', tools.value);
|
||||
emitToolsUpdate();
|
||||
}
|
||||
else {
|
||||
error.value = '接口返回数据格式不正确';
|
||||
console.error('接口返回数据格式错误:', res);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
error.value = '获取工具列表失败,请检查网络连接';
|
||||
console.error('获取工具列表失败:', err);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新工具列表
|
||||
async function refreshTools() {
|
||||
await getAiChatToolList();
|
||||
}
|
||||
|
||||
// 组件挂载时获取工具列表
|
||||
onMounted(() => {
|
||||
getAiChatToolList();
|
||||
});
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
selectedTools,
|
||||
selectedApiTools,
|
||||
clearSelection,
|
||||
selectAll,
|
||||
setToolEnabled,
|
||||
setToolEnabledByApiName,
|
||||
refreshTools,
|
||||
getAiChatToolList,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tools-container">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<el-icon class="loading-icon">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<span>加载工具中...</span>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-state">
|
||||
<el-icon class="error-icon">
|
||||
<ElementPlus />
|
||||
</el-icon>
|
||||
<span>{{ error }}</span>
|
||||
<el-button type="text" class="retry-btn" @click="refreshTools">
|
||||
重试
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="tools.length === 0" class="empty-state">
|
||||
<span>暂无可用工具</span>
|
||||
</div>
|
||||
|
||||
<!-- 工具按钮 -->
|
||||
<div
|
||||
v-for="tool in tools"
|
||||
v-else
|
||||
:key="tool.id"
|
||||
class="tool-item"
|
||||
:class="{
|
||||
active: isActive(tool.id),
|
||||
disabled: !tool.enabled,
|
||||
}"
|
||||
:title="tool.tip"
|
||||
@click="handleToolClick(tool)"
|
||||
>
|
||||
<el-icon class="tool-icon">
|
||||
<component :is="tool.icon" />
|
||||
</el-icon>
|
||||
<span class="tool-text">{{ tool.name }}</span>
|
||||
|
||||
<!-- 选中指示器 -->
|
||||
<!-- <span v-if="isActive(tool.id)" class="check-indicator">✓</span> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 调试信息 -->
|
||||
<div v-if="false" class="debug-info">
|
||||
<div>工具数量: {{ tools.length }}</div>
|
||||
<div>当前选中ID: {{ activeToolIds }}</div>
|
||||
<div>选中的API工具: {{ selectedApiTools }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tools-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
.loading-icon {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
color: #f56c6c;
|
||||
|
||||
.error-icon {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-left: 8px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: #c0c4cc;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
background: white;
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border-color: #409EFF;
|
||||
color: #409EFF;
|
||||
|
||||
.tool-icon {
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tool-text {
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.check-indicator {
|
||||
margin-left: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
|
||||
div {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user