ai-chat-ui/src/App.vue

227 lines
4.8 KiB
Vue

<template>
<div class="app" :class="{ dark: isDark }">
<!-- 侧边栏 -->
<ChatSidebar />
<!-- 主内容区 -->
<ChatMain ref="chatMainRef" @toggle-sidebar="toggleSidebar" />
<!-- 模态框 -->
<SearchModal />
<ShortcutsModal />
<SettingsModal />
<ConversationSettingsModal />
<!-- Toast 通知 -->
<Teleport to="body">
<TransitionGroup name="toast" tag="div" class="toast-container">
<div
v-for="toast in toasts"
:key="toast.id"
class="toast"
:class="toast.type"
>
<Check v-if="toast.type === 'success'" :size="18" />
<AlertCircle v-else-if="toast.type === 'error'" :size="18" />
<Info v-else :size="18" />
<span>{{ toast.message }}</span>
</div>
</TransitionGroup>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { useChatStore } from "@/stores/chat";
import { useSettingsStore } from "@/stores/settings";
import { useKeyboard, getDefaultShortcuts } from "@/composables/useKeyboard";
import ChatSidebar from "@/components/sidebar/ChatSidebar.vue";
import ChatMain from "@/components/chat/ChatMain.vue";
import SearchModal from "@/components/modals/SearchModal.vue";
import ShortcutsModal from "@/components/modals/ShortcutsModal.vue";
import SettingsModal from "@/components/modals/SettingsModal.vue";
import ConversationSettingsModal from "@/components/modals/ConversationSettingsModal.vue";
import { Check, AlertCircle, Info } from "@/components/icons";
import { useAuthStore } from "./stores/auth";
const authStore = useAuthStore();
// Stores
const chatStore = useChatStore();
const settingsStore = useSettingsStore();
const { settings } = storeToRefs(settingsStore);
// Refs
const chatMainRef = ref<InstanceType<typeof ChatMain> | null>(null);
// 计算属性
const isDark = computed(() => {
if (settings.value.theme === "system") {
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
return settings.value.theme === "dark";
});
// Toast 通知系统
interface Toast {
id: number;
message: string;
type: "success" | "error" | "info";
}
const toasts = ref<Toast[]>([]);
let toastId = 0;
function showToast(message: string, type: Toast["type"] = "info") {
const id = ++toastId;
toasts.value.push({ id, message, type });
setTimeout(() => {
const index = toasts.value.findIndex((t) => t.id === id);
if (index !== -1) {
toasts.value.splice(index, 1);
}
}, 3000);
}
// 方法
function toggleSidebar() {
settingsStore.toggleSidebar();
}
function newChat() {
chatStore.createConversation();
showToast("已创建新对话", "success");
}
function focusInput() {
chatMainRef.value?.focusInput();
}
// 快捷键
useKeyboard(
getDefaultShortcuts({
newChat,
toggleSidebar,
focusInput,
sendMessage: () => {}, // 由 ChatInput 内部处理
cancelStream: () => {
if (chatStore.isStreaming) {
chatStore.stopStreaming();
showToast("已停止生成", "info");
}
},
toggleTheme: () => {
settingsStore.toggleTheme();
showToast(`主题已切换为 ${settings.value.theme}`, "success");
},
showShortcuts: () => {
settingsStore.openShortcutsModal();
},
searchConversations: () => {
settingsStore.openSearchModal();
},
}),
);
// 初始化
onMounted(() => {
authStore.init();
console.log(authStore.token);
// // 如果没有对话,创建一个
// if (chatStore.conversations.length === 0) {
// chatStore.createConversation();
// }
});
// 暴露给全局使用
window.$toast = showToast;
</script>
<style lang="scss">
.app {
display: flex;
width: 100vw;
height: 100vh;
overflow: hidden;
background: #f5f5f5;
&.dark {
background: #11111b;
color: #e5e7eb;
}
}
// Toast 样式
.toast-container {
position: fixed;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 9999;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 20px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
font-size: 14px;
font-weight: 500;
color: #374151;
pointer-events: auto;
.dark & {
background: #2d2d3d;
color: #e5e7eb;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
}
&.success {
svg {
color: #10b981;
}
}
&.error {
svg {
color: #ef4444;
}
}
&.info {
svg {
color: #3b82f6;
}
}
}
// Toast 动画
.toast-enter-active,
.toast-leave-active {
transition: all 0.3s ease;
}
.toast-enter-from {
opacity: 0;
transform: translateX(100px);
}
.toast-leave-to {
opacity: 0;
transform: translateX(100px);
}
.toast-move {
transition: transform 0.3s ease;
}
</style>