Compare commits
5 Commits
e80feb46d6
...
b4af7e2d55
| Author | SHA1 | Date |
|---|---|---|
|
|
b4af7e2d55 | 2 months ago |
|
|
9026f20d8a | 2 months ago |
|
|
cc350117a7 | 2 months ago |
|
|
006ef674f4 | 2 months ago |
|
|
43dc8b6da2 | 2 months ago |
12 changed files with 497 additions and 24 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