232 lines
5.3 KiB
Vue
232 lines
5.3 KiB
Vue
<template>
|
|
<div class="chat-layout">
|
|
<div
|
|
class="sidebar-overlay"
|
|
:class="{ active: chat.sidebarOpen }"
|
|
@click="chat.closeSidebar"
|
|
/>
|
|
|
|
<ChatSidebar />
|
|
|
|
<main class="chat-main">
|
|
<header class="chat-header">
|
|
<button class="menu-btn" type="button" aria-label="打开会话列表" @click="chat.toggleSidebar">
|
|
<IconMenu2 :size="21" :stroke-width="1.8" />
|
|
</button>
|
|
<div class="chat-heading">
|
|
<span class="chat-kicker">AI 创作空间</span>
|
|
<h2 class="chat-title">{{ currentTitle }}</h2>
|
|
</div>
|
|
<div class="header-actions">
|
|
<a
|
|
v-if="auth.user?.role === 'admin'"
|
|
:href="adminUrl"
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="btn btn-ghost btn-sm admin-link"
|
|
aria-label="打开管理后台"
|
|
>
|
|
<IconSettings :size="17" :stroke-width="1.8" />
|
|
<span class="action-label">后台</span>
|
|
</a>
|
|
<button
|
|
class="btn btn-ghost btn-sm"
|
|
type="button"
|
|
:aria-label="auth.isGuest ? '登录账户' : '退出登录'"
|
|
@click="handleAccountAction"
|
|
>
|
|
<IconLogin v-if="auth.isGuest" :size="17" :stroke-width="1.8" />
|
|
<IconLogout v-else :size="17" :stroke-width="1.8" />
|
|
<span class="action-label">{{ auth.isGuest ? '登录' : '退出' }}</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<MessageList
|
|
:messages="chat.messages"
|
|
:loading="chat.loading"
|
|
:streaming="chat.streamingContent"
|
|
:streaming-attachments="chat.streamingAttachments"
|
|
:sending="chat.sending"
|
|
/>
|
|
|
|
<ChatInput @send="handleSend" />
|
|
</main>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { useChatStore } from '@/stores/chat'
|
|
import { useNotificationStore } from '@/stores/notification'
|
|
import { useSettingsStore } from '@/stores/settings'
|
|
import ChatSidebar from '@/components/ChatSidebar.vue'
|
|
import MessageList from '@/components/MessageList.vue'
|
|
import ChatInput from '@/components/ChatInput.vue'
|
|
import IconLogout from '@tabler/icons-vue/dist/esm/icons/IconLogout.mjs'
|
|
import IconLogin from '@tabler/icons-vue/dist/esm/icons/IconLogin.mjs'
|
|
import IconMenu2 from '@tabler/icons-vue/dist/esm/icons/IconMenu2.mjs'
|
|
import IconSettings from '@tabler/icons-vue/dist/esm/icons/IconSettings.mjs'
|
|
|
|
const router = useRouter()
|
|
const auth = useAuthStore()
|
|
const chat = useChatStore()
|
|
const notification = useNotificationStore()
|
|
const settings = useSettingsStore()
|
|
|
|
const currentTitle = computed(() => {
|
|
const conv = chat.conversations.find(c => c.id === chat.currentId)
|
|
return conv?.title || '新对话'
|
|
})
|
|
|
|
const adminUrl = import.meta.env.VITE_ADMIN_URL || `${window.location.protocol}//${window.location.hostname}:5174`
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
await settings.loadPublic()
|
|
await Promise.all([settings.loadModels(), settings.loadAgents()])
|
|
await chat.fetchConversations()
|
|
} catch (err) {
|
|
notification.error(err, { title: '页面加载失败' })
|
|
}
|
|
})
|
|
|
|
async function handleSend({ content, attachments, agentId }) {
|
|
try {
|
|
await chat.sendMessage(content, attachments, agentId)
|
|
} catch (e) {
|
|
notification.error(e)
|
|
}
|
|
}
|
|
|
|
function handleAccountAction() {
|
|
if (auth.isGuest) {
|
|
router.push('/login')
|
|
return
|
|
}
|
|
|
|
auth.logout()
|
|
router.push('/login')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.chat-layout {
|
|
display: flex;
|
|
height: 100%;
|
|
min-height: 100dvh;
|
|
overflow: hidden;
|
|
background: linear-gradient(180deg, #f8faff 0%, var(--bg-primary) 100%);
|
|
}
|
|
|
|
.chat-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
background: transparent;
|
|
}
|
|
|
|
.chat-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
height: var(--header-height);
|
|
padding: 0 22px;
|
|
border-bottom: 1px solid rgba(225, 231, 239, 0.92);
|
|
background: rgba(255, 255, 255, 0.86);
|
|
backdrop-filter: blur(14px);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.menu-btn {
|
|
display: none;
|
|
width: 36px;
|
|
height: 36px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 50%;
|
|
color: var(--text-secondary);
|
|
transition: background 0.16s ease, color 0.16s ease, transform 0.16s ease;
|
|
}
|
|
|
|
.menu-btn:hover {
|
|
background: var(--bg-soft);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.menu-btn:active {
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
.chat-heading {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.chat-kicker {
|
|
display: inline-block;
|
|
margin-bottom: 1px;
|
|
font-size: 10px;
|
|
letter-spacing: 0.04em;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.chat-title {
|
|
font-size: 16px;
|
|
font-weight: 650;
|
|
line-height: 1.25;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn-sm {
|
|
min-height: 36px;
|
|
padding: 7px 12px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.chat-header {
|
|
padding: 0 10px 0 8px;
|
|
}
|
|
|
|
.menu-btn {
|
|
display: flex;
|
|
}
|
|
|
|
.chat-kicker {
|
|
display: none;
|
|
}
|
|
|
|
.chat-title {
|
|
font-size: 17px;
|
|
}
|
|
|
|
.btn-sm {
|
|
width: 36px;
|
|
min-height: 36px;
|
|
padding: 0;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.action-label {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.menu-btn {
|
|
transition: none;
|
|
}
|
|
}
|
|
</style>
|