|
|
|
@ -7,6 +7,7 @@ const props = defineProps<{ |
|
|
|
loggedIn: boolean; |
|
|
|
user: { name?: string | null; email?: string | null; role?: string | null } | null; |
|
|
|
collapsed: boolean; |
|
|
|
mobile?: boolean; |
|
|
|
}>(); |
|
|
|
|
|
|
|
const emit = defineEmits<{ |
|
|
|
@ -16,13 +17,15 @@ const emit = defineEmits<{ |
|
|
|
delete: [id: string]; |
|
|
|
login: []; |
|
|
|
toggle: []; |
|
|
|
close: []; |
|
|
|
}>(); |
|
|
|
|
|
|
|
const isAdmin = computed(() => props.user?.role === "admin"); |
|
|
|
</script> |
|
|
|
|
|
|
|
<template> |
|
|
|
<aside class="agent-sidebar" :class="{ collapsed }"> |
|
|
|
<!-- Desktop: inline flex sidebar (hidden on mobile via CSS) --> |
|
|
|
<aside class="agent-sidebar desktop" :class="{ collapsed }"> |
|
|
|
<div class="sidebar-header"> |
|
|
|
<button class="new-chat-btn" @click="emit('newSession')"> |
|
|
|
<Icon name="lucide:plus" /> |
|
|
|
@ -78,6 +81,77 @@ const isAdmin = computed(() => props.user?.role === "admin"); |
|
|
|
</template> |
|
|
|
</div> |
|
|
|
</aside> |
|
|
|
|
|
|
|
<!-- Mobile: overlay drawer (hidden on desktop via CSS) --> |
|
|
|
<ClientOnly> |
|
|
|
<Teleport to="body"> |
|
|
|
<Transition name="sidebar-overlay"> |
|
|
|
<div |
|
|
|
v-if="mobile && !collapsed" |
|
|
|
class="sidebar-overlay" |
|
|
|
@click="emit('close')" |
|
|
|
/> |
|
|
|
</Transition> |
|
|
|
<Transition name="sidebar-slide"> |
|
|
|
<aside v-if="mobile && !collapsed" class="agent-sidebar mobile"> |
|
|
|
<div class="sidebar-header"> |
|
|
|
<button class="new-chat-btn" @click="emit('newSession')"> |
|
|
|
<Icon name="lucide:plus" /> |
|
|
|
<span>新对话</span> |
|
|
|
</button> |
|
|
|
<button class="collapse-btn" @click="emit('close')"> |
|
|
|
<Icon name="lucide:x" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="sidebar-sessions"> |
|
|
|
<AgentSidebarItem |
|
|
|
v-for="s in sessions" |
|
|
|
:key="s.id" |
|
|
|
:session="s" |
|
|
|
:active="s.id === currentSessionId" |
|
|
|
@select="emit('select', s.id)" |
|
|
|
@rename="(title) => emit('rename', s.id, title)" |
|
|
|
@delete="emit('delete', s.id)" |
|
|
|
/> |
|
|
|
<div v-if="sessions.length === 0" class="empty-sessions"> |
|
|
|
暂无对话 |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="sidebar-footer"> |
|
|
|
<div v-if="loggedIn" class="footer-links"> |
|
|
|
<NuxtLink to="/settings" class="footer-link"> |
|
|
|
<Icon name="lucide:settings" /> |
|
|
|
<span>设置</span> |
|
|
|
</NuxtLink> |
|
|
|
<NuxtLink v-if="isAdmin" to="/admin/dashboard" class="footer-link"> |
|
|
|
<Icon name="lucide:shield" /> |
|
|
|
<span>管理后台</span> |
|
|
|
</NuxtLink> |
|
|
|
</div> |
|
|
|
<template v-if="loggedIn && user"> |
|
|
|
<div class="user-info"> |
|
|
|
<div class="user-avatar"> |
|
|
|
{{ (user.name || user.email || "?").charAt(0).toUpperCase() }} |
|
|
|
</div> |
|
|
|
<div class="user-meta"> |
|
|
|
<div class="user-name">{{ user.name || "用户" }}</div> |
|
|
|
<div class="user-email">{{ user.email }}</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
<template v-else> |
|
|
|
<button class="login-btn" @click="emit('login')"> |
|
|
|
<Icon name="lucide:log-in" /> |
|
|
|
<span>登录以保存对话</span> |
|
|
|
</button> |
|
|
|
</template> |
|
|
|
</div> |
|
|
|
</aside> |
|
|
|
</Transition> |
|
|
|
</Teleport> |
|
|
|
</ClientOnly> |
|
|
|
</template> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
@ -97,6 +171,13 @@ const isAdmin = computed(() => props.user?.role === "admin"); |
|
|
|
border-right: none; |
|
|
|
} |
|
|
|
|
|
|
|
/* Hide desktop sidebar on mobile */ |
|
|
|
@media (max-width: 767.98px) { |
|
|
|
.agent-sidebar.desktop { |
|
|
|
display: none; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.sidebar-header { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
@ -252,4 +333,44 @@ const isAdmin = computed(() => props.user?.role === "admin"); |
|
|
|
border-color: var(--color-primary); |
|
|
|
color: var(--color-primary); |
|
|
|
} |
|
|
|
|
|
|
|
/* ── Mobile overlay drawer ── */ |
|
|
|
|
|
|
|
.sidebar-overlay { |
|
|
|
position: fixed; |
|
|
|
inset: 0; |
|
|
|
background: rgba(0, 0, 0, 0.4); |
|
|
|
z-index: 199; |
|
|
|
} |
|
|
|
|
|
|
|
.agent-sidebar.mobile { |
|
|
|
position: fixed; |
|
|
|
top: 0; |
|
|
|
left: 0; |
|
|
|
bottom: 0; |
|
|
|
width: 280px; |
|
|
|
max-width: 85vw; |
|
|
|
z-index: 200; |
|
|
|
box-shadow: 2px 0 16px rgba(0, 0, 0, 0.15); |
|
|
|
} |
|
|
|
|
|
|
|
.sidebar-overlay-enter-active, |
|
|
|
.sidebar-overlay-leave-active { |
|
|
|
transition: opacity 0.2s ease; |
|
|
|
} |
|
|
|
|
|
|
|
.sidebar-overlay-enter-from, |
|
|
|
.sidebar-overlay-leave-to { |
|
|
|
opacity: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.sidebar-slide-enter-active, |
|
|
|
.sidebar-slide-leave-active { |
|
|
|
transition: transform 0.25s ease; |
|
|
|
} |
|
|
|
|
|
|
|
.sidebar-slide-enter-from, |
|
|
|
.sidebar-slide-leave-to { |
|
|
|
transform: translateX(-100%); |
|
|
|
} |
|
|
|
</style> |
|
|
|
|