feat: 功能2 - 绑定/解绑 Agent (v0.9.7)
- 添加 BotSettingsModal.vue 机器人设置模态框 - 显示在线 Agent 列表 - 实现 Agent 绑定功能 - 实现 Agent 解绑功能 - HomeView 添加设置按钮(hover 显示) - Store 添加 bindBotAgent 方法 - 显示机器人绑定状态
This commit is contained in:
410
frontend/src/components/chat/BotSettingsModal.vue
Normal file
410
frontend/src/components/chat/BotSettingsModal.vue
Normal file
@@ -0,0 +1,410 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { Bot, Agent } from '@/stores/chat'
|
||||
|
||||
const props = defineProps<{
|
||||
bot: Bot
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
updated: [bot: Bot]
|
||||
}>()
|
||||
|
||||
const agents = ref<Agent[]>([])
|
||||
const selectedAgentId = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchAgents()
|
||||
selectedAgentId.value = props.bot.agent_id || null
|
||||
})
|
||||
|
||||
async function fetchAgents() {
|
||||
try {
|
||||
const response = await fetch('/api/agents/available', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
})
|
||||
const data = await response.json()
|
||||
agents.value = data.agents || []
|
||||
} catch (e) {
|
||||
console.error('Fetch agents failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBind() {
|
||||
if (!selectedAgentId.value) {
|
||||
error.value = '请选择一个 Agent'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/bots/${props.bot.id}/bind`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
},
|
||||
body: JSON.stringify({ agent_id: selectedAgentId.value })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.error || '绑定失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
emit('updated', data.bot)
|
||||
emit('close')
|
||||
} catch (e: any) {
|
||||
error.value = e.message || '绑定失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnbind() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/bots/${props.bot.id}/unbind`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.error || '解绑失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
selectedAgentId.value = null
|
||||
emit('updated', data.bot)
|
||||
} catch (e: any) {
|
||||
error.value = e.message || '解绑失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal-overlay" @click.self="emit('close')">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>机器人设置</h3>
|
||||
<button class="close-btn" @click="emit('close')">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Bot Info -->
|
||||
<div class="bot-info">
|
||||
<div class="bot-avatar">{{ bot.avatar || '🤖' }}</div>
|
||||
<div class="bot-details">
|
||||
<h4>{{ bot.display_name || bot.name }}</h4>
|
||||
<p class="bot-status" :class="bot.status">
|
||||
{{ bot.status === 'online' ? '🟢 在线' : '⚪ 离线' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Selection -->
|
||||
<div class="form-group">
|
||||
<label>绑定 Agent</label>
|
||||
<p class="hint">选择一个在线的 Agent 来启用此机器人</p>
|
||||
|
||||
<div class="agent-list" v-if="agents.length > 0">
|
||||
<label
|
||||
v-for="agent in agents"
|
||||
:key="agent.id"
|
||||
class="agent-item"
|
||||
:class="{ selected: selectedAgentId === agent.id }"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
:value="agent.id"
|
||||
v-model="selectedAgentId"
|
||||
/>
|
||||
<div class="agent-avatar">🤖</div>
|
||||
<div class="agent-info">
|
||||
<span class="agent-name">{{ agent.display_name || agent.name }}</span>
|
||||
<span class="agent-status" :class="agent.status">
|
||||
{{ agent.status === 'online' ? '在线' : '离线' }}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="empty" v-else>
|
||||
<p>暂无可用 Agent</p>
|
||||
<p class="hint">请确保至少有一个 Agent 在线</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="modal-actions">
|
||||
<button
|
||||
v-if="bot.agent_id"
|
||||
type="button"
|
||||
class="btn-unbind"
|
||||
@click="handleUnbind"
|
||||
:disabled="loading"
|
||||
>
|
||||
解绑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-bind"
|
||||
@click="handleBind"
|
||||
:disabled="loading || !selectedAgentId"
|
||||
>
|
||||
{{ loading ? '处理中...' : '绑定' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--border-radius-lg);
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: var(--spacing-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.bot-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.bot-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.bot-details h4 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.bot-status {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.bot-status.online {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.agent-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.agent-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.agent-item:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.agent-item.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.agent-item input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.agent-status {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.agent-status.online {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: var(--spacing-lg);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--error-color);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--spacing-sm);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.btn-unbind,
|
||||
.btn-bind {
|
||||
flex: 1;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-unbind {
|
||||
background: transparent;
|
||||
border: 1px solid var(--error-color);
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.btn-unbind:hover:not(:disabled) {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.btn-bind {
|
||||
background: var(--primary-color);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-bind:hover:not(:disabled) {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-unbind:disabled,
|
||||
.btn-bind:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -2,14 +2,17 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useChatStore, type Bot } from '@/stores/chat'
|
||||
import CreateBotModal from '@/components/chat/CreateBotModal.vue'
|
||||
import BotSettingsModal from '@/components/chat/BotSettingsModal.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const chatStore = useChatStore()
|
||||
|
||||
const showCreateBot = ref(false)
|
||||
const showBotSettings = ref(false)
|
||||
const selectedBot = ref<Bot | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
await chatStore.fetchBots()
|
||||
@@ -28,9 +31,23 @@ function resumeChat(sessionId: string) {
|
||||
router.push({ name: 'chat', params: { sessionId } })
|
||||
}
|
||||
|
||||
function handleBotCreated(bot: any) {
|
||||
function handleBotCreated(bot: Bot) {
|
||||
chatStore.bots.push(bot)
|
||||
}
|
||||
|
||||
function openBotSettings(bot: Bot, event: Event) {
|
||||
event.stopPropagation()
|
||||
selectedBot.value = bot
|
||||
showBotSettings.value = true
|
||||
}
|
||||
|
||||
function handleBotUpdated(bot: Bot) {
|
||||
const index = chatStore.bots.findIndex(b => b.id === bot.id)
|
||||
if (index !== -1) {
|
||||
chatStore.bots[index] = bot
|
||||
}
|
||||
selectedBot.value = bot
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -53,7 +70,10 @@ function handleBotCreated(bot: any) {
|
||||
<main class="main-content">
|
||||
<!-- Bot List -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">选择机器人开始聊天</h2>
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">选择机器人开始聊天</h2>
|
||||
<span class="bot-count">{{ chatStore.bots.length }} 个机器人</span>
|
||||
</div>
|
||||
<div class="bot-grid">
|
||||
<div
|
||||
v-for="bot in chatStore.bots"
|
||||
@@ -70,6 +90,12 @@ function handleBotCreated(bot: any) {
|
||||
{{ bot.status === 'online' ? '🟢 在线' : '⚪ 离线' }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="settings-btn"
|
||||
@click="openBotSettings(bot, $event)"
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -110,6 +136,13 @@ function handleBotCreated(bot: any) {
|
||||
@close="showCreateBot = false"
|
||||
@created="handleBotCreated"
|
||||
/>
|
||||
|
||||
<BotSettingsModal
|
||||
v-if="showBotSettings && selectedBot"
|
||||
:bot="selectedBot"
|
||||
@close="showBotSettings = false"
|
||||
@updated="handleBotUpdated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -192,11 +225,22 @@ function handleBotCreated(bot: any) {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.bot-count {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.bot-grid {
|
||||
@@ -214,6 +258,7 @@ function handleBotCreated(bot: any) {
|
||||
gap: var(--spacing-md);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bot-card:hover {
|
||||
@@ -221,6 +266,32 @@ function handleBotCreated(bot: any) {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.bot-card:hover .settings-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settings-btn:hover {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.bot-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
Reference in New Issue
Block a user