feat:聊天tool前端入口

This commit is contained in:
Gsh
2025-12-23 00:15:32 +08:00
parent 8f515f76c0
commit 681194a517
6 changed files with 431 additions and 182 deletions

View 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>