ai-chat-ui/src/App.vue

156 lines
3.0 KiB
Vue

<template>
<n-config-provider>
<n-message-provider>
<div class="app" :class="{ dark: isDark }">
<router-view />
<!-- 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>
</n-message-provider>
</n-config-provider>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { storeToRefs } from "pinia";
import { useSettingsStore } from "@/stores/settings";
import { Check, AlertCircle, Info } from "@/components/icons";
import { NConfigProvider, NMessageProvider } from "naive-ui";
const settingsStore = useSettingsStore();
const { settings } = storeToRefs(settingsStore);
// 计算属性
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);
}
// 暴露给全局使用
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>