Files
chat/frontend/src/views/ChatView.vue
T
2026-07-17 17:28:25 +08:00

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>