6 changed files with 324 additions and 21 deletions
@ -0,0 +1,247 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
interface AgentInfo { |
||||
|
slug: string; |
||||
|
name: string; |
||||
|
description?: string | null; |
||||
|
} |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
currentSlug: string; |
||||
|
}>(); |
||||
|
|
||||
|
const emit = defineEmits<{ |
||||
|
switch: [slug: string]; |
||||
|
}>(); |
||||
|
|
||||
|
const open = ref(false); |
||||
|
const agents = ref<AgentInfo[]>([]); |
||||
|
const loading = ref(false); |
||||
|
const dropdownRef = ref<HTMLElement | null>(null); |
||||
|
|
||||
|
const currentAgent = computed(() => |
||||
|
agents.value.find((a) => a.slug === props.currentSlug) ?? null, |
||||
|
); |
||||
|
const displayName = computed(() => currentAgent.value?.name ?? "智能助手"); |
||||
|
|
||||
|
async function loadAgents() { |
||||
|
loading.value = true; |
||||
|
try { |
||||
|
const res = await $fetch<{ code: number; data: { list: AgentInfo[] } }>("/api/agents"); |
||||
|
agents.value = res.data?.list ?? []; |
||||
|
} catch { |
||||
|
agents.value = []; |
||||
|
} finally { |
||||
|
loading.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function toggle() { |
||||
|
if (!open.value && agents.value.length === 0) { |
||||
|
loadAgents(); |
||||
|
} |
||||
|
open.value = !open.value; |
||||
|
} |
||||
|
|
||||
|
function pick(slug: string) { |
||||
|
open.value = false; |
||||
|
if (slug !== props.currentSlug) { |
||||
|
emit("switch", slug); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleClickOutside(e: MouseEvent) { |
||||
|
if (dropdownRef.value && !dropdownRef.value.contains(e.target as Node)) { |
||||
|
open.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMounted(() => { |
||||
|
document.addEventListener("click", handleClickOutside); |
||||
|
}); |
||||
|
|
||||
|
onBeforeUnmount(() => { |
||||
|
document.removeEventListener("click", handleClickOutside); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div ref="dropdownRef" class="agent-switcher"> |
||||
|
<button class="switcher-trigger" :class="{ active: open }" @click="toggle"> |
||||
|
<Icon name="lucide:bot" class="trigger-icon" /> |
||||
|
<span class="trigger-name">{{ displayName }}</span> |
||||
|
<Icon name="lucide:chevron-down" class="trigger-arrow" :class="{ rotated: open }" /> |
||||
|
</button> |
||||
|
|
||||
|
<Transition name="dropdown"> |
||||
|
<div v-if="open" class="switcher-dropdown"> |
||||
|
<div v-if="loading" class="dropdown-loading">加载中…</div> |
||||
|
<template v-else> |
||||
|
<button |
||||
|
v-for="a in agents" |
||||
|
:key="a.slug" |
||||
|
class="dropdown-item" |
||||
|
:class="{ selected: a.slug === currentSlug }" |
||||
|
@click="pick(a.slug)" |
||||
|
> |
||||
|
<Icon |
||||
|
:name="a.slug === currentSlug ? 'lucide:check' : 'lucide:bot'" |
||||
|
class="item-icon" |
||||
|
/> |
||||
|
<div class="item-meta"> |
||||
|
<div class="item-name">{{ a.name }}</div> |
||||
|
<div v-if="a.description" class="item-desc">{{ a.description }}</div> |
||||
|
</div> |
||||
|
</button> |
||||
|
<div v-if="agents.length === 0" class="dropdown-empty">暂无可用 agent</div> |
||||
|
</template> |
||||
|
</div> |
||||
|
</Transition> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.agent-switcher { |
||||
|
position: relative; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.switcher-trigger { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
width: 100%; |
||||
|
padding: 8px 10px; |
||||
|
background: var(--color-surface-card); |
||||
|
border: 1px solid var(--color-hairline); |
||||
|
border-radius: 10px; |
||||
|
font-size: 13px; |
||||
|
font-weight: 500; |
||||
|
color: var(--color-ink); |
||||
|
cursor: pointer; |
||||
|
transition: all 0.15s; |
||||
|
} |
||||
|
|
||||
|
.switcher-trigger:hover { |
||||
|
border-color: var(--color-primary); |
||||
|
background: var(--color-canvas); |
||||
|
} |
||||
|
|
||||
|
.switcher-trigger.active { |
||||
|
border-color: var(--color-primary); |
||||
|
} |
||||
|
|
||||
|
.trigger-icon { |
||||
|
font-size: 15px; |
||||
|
color: var(--color-primary); |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.trigger-name { |
||||
|
flex: 1; |
||||
|
text-align: left; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.trigger-arrow { |
||||
|
font-size: 14px; |
||||
|
color: var(--color-muted); |
||||
|
flex-shrink: 0; |
||||
|
transition: transform 0.2s; |
||||
|
} |
||||
|
|
||||
|
.trigger-arrow.rotated { |
||||
|
transform: rotate(180deg); |
||||
|
} |
||||
|
|
||||
|
.switcher-dropdown { |
||||
|
position: absolute; |
||||
|
top: calc(100% + 4px); |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
background: var(--color-canvas); |
||||
|
border: 1px solid var(--color-hairline); |
||||
|
border-radius: 10px; |
||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); |
||||
|
z-index: 50; |
||||
|
max-height: 320px; |
||||
|
overflow-y: auto; |
||||
|
padding: 4px; |
||||
|
} |
||||
|
|
||||
|
.dropdown-loading, |
||||
|
.dropdown-empty { |
||||
|
padding: 16px; |
||||
|
text-align: center; |
||||
|
font-size: 13px; |
||||
|
color: var(--color-muted); |
||||
|
} |
||||
|
|
||||
|
.dropdown-item { |
||||
|
display: flex; |
||||
|
align-items: flex-start; |
||||
|
gap: 8px; |
||||
|
width: 100%; |
||||
|
padding: 8px 10px; |
||||
|
border: none; |
||||
|
background: none; |
||||
|
border-radius: 8px; |
||||
|
cursor: pointer; |
||||
|
text-align: left; |
||||
|
transition: background 0.12s; |
||||
|
} |
||||
|
|
||||
|
.dropdown-item:hover { |
||||
|
background: var(--color-surface-soft); |
||||
|
} |
||||
|
|
||||
|
.dropdown-item.selected { |
||||
|
background: var(--color-surface-soft); |
||||
|
} |
||||
|
|
||||
|
.item-icon { |
||||
|
font-size: 15px; |
||||
|
color: var(--color-primary); |
||||
|
flex-shrink: 0; |
||||
|
margin-top: 1px; |
||||
|
} |
||||
|
|
||||
|
.dropdown-item.selected .item-icon { |
||||
|
color: var(--color-primary); |
||||
|
} |
||||
|
|
||||
|
.item-meta { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
|
||||
|
.item-name { |
||||
|
font-size: 13px; |
||||
|
font-weight: 500; |
||||
|
color: var(--color-ink); |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.item-desc { |
||||
|
font-size: 11px; |
||||
|
color: var(--color-muted); |
||||
|
margin-top: 2px; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.dropdown-enter-active, |
||||
|
.dropdown-leave-active { |
||||
|
transition: opacity 0.15s, transform 0.15s; |
||||
|
} |
||||
|
|
||||
|
.dropdown-enter-from, |
||||
|
.dropdown-leave-to { |
||||
|
opacity: 0; |
||||
|
transform: translateY(-4px); |
||||
|
} |
||||
|
</style> |
||||
Binary file not shown.
Loading…
Reference in new issue