feat(ui): add branded startup loading animation

This commit is contained in:
digua committed 2026-07-23 22:05:18 +08:00
1 parent c6676e68a5
commit ac64cb72fb
5 files changed
+178 -11

No files matched your search

+2 -4
View File
@@ -14,6 +14,7 @@ import { reportError } from '@/services/log-report'
import { useLayoutStore } from '@/stores/layout'
import { useSessionStore } from '@/stores/session'
import { useSettingsStore } from '@/stores/settings'
import { StartupLoading } from '@/components/UI'
import { PLATFORM_CAPABILITIES } from '@/utils/platform-capabilities'
import {
WebWasmWorkspaceBusyError,
@@ -119,10 +120,7 @@ onBeforeUnmount(() => {
{{ t('common.retry') }}
</UButton>
</div>
<div v-else class="flex flex-col items-center justify-center text-center">
<UIcon name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-pink-500" />
<p class="mt-2 text-sm text-gray-500">{{ t('common.initializing') }}</p>
</div>
<StartupLoading v-else />
</div>
</template>
<template v-else>
+5 -5
View File
@@ -11,6 +11,7 @@ import SettingsModal from '@/components/common/SettingsModal.vue'
import { ChatRecordDrawer } from '@/components/common/ChatRecord'
import GlobalTaskBar from '@/components/AIChat/GlobalTaskBar.vue'
import DebugToolsPanel from '@/components/layout/DebugToolsPanel.vue'
import { StartupLoading } from '@/components/UI'
import { useSessionStore } from '@/stores/session'
import { useLayoutStore } from '@/stores/layout'
import { useSettingsStore } from '@/stores/settings'
@@ -198,10 +199,7 @@ onUnmounted(() => {
{{ t('common.retry') }}
</UButton>
</div>
<div v-else class="flex flex-col items-center justify-center text-center">
<UIcon name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-pink-500" />
<p class="mt-2 text-sm text-gray-500">{{ t('common.initializing') }}</p>
</div>
<StartupLoading v-else />
</div>
</template>
<template v-else>
@@ -240,7 +238,9 @@ onUnmounted(() => {
class="pointer-events-auto m-0 h-screen max-h-none w-screen max-w-none border-0 bg-white p-0 outline-none backdrop:bg-white dark:bg-page-dark dark:backdrop:bg-page-dark"
tabindex="-1"
@cancel.prevent
/>
>
<StartupLoading v-if="isBootstrapMaskVisible" />
</dialog>
</Teleport>
</UApp>
</template>
+2 -2
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
/**
* 统一加载状态组件
* 支持三种场景:行内加载、页面加载、蒙层覆盖
* 支持行内加载、页面加载和蒙层覆盖
*/
import { computed } from 'vue'
@@ -36,7 +36,7 @@ const containerClass = computed(() => {
</script>
<template>
<div :class="containerClass">
<div :class="containerClass" role="status" aria-live="polite">
<div class="flex flex-col items-center justify-center text-center">
<UIcon name="i-heroicons-arrow-path" class="h-6 w-6 animate-spin text-pink-500" />
<p v-if="text" class="mt-2 text-sm text-gray-500">{{ text }}</p>
+168
View File
@@ -0,0 +1,168 @@
<script setup lang="ts">
import logoSvg from '@/assets/images/logo.svg'
</script>
<template>
<div
class="startup-loader flex h-full w-full items-center justify-center overflow-hidden"
role="status"
aria-live="polite"
aria-label="ChatLab"
>
<div class="startup-loader__stage">
<div class="startup-loader__glow" aria-hidden="true"></div>
<div class="startup-loader__brand">
<span class="startup-loader__logo-wrap" aria-hidden="true">
<img :src="logoSvg" alt="" class="startup-loader__logo" />
</span>
<span class="startup-loader__name text-gray-900 dark:text-white">ChatLab</span>
</div>
</div>
</div>
</template>
<style scoped>
.startup-loader__stage {
position: relative;
display: flex;
width: min(18rem, calc(100vw - 3rem));
height: 6rem;
align-items: center;
justify-content: center;
isolation: isolate;
}
.startup-loader__glow {
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent 0%,
color-mix(in srgb, var(--color-pink-500) 7%, transparent) 34%,
color-mix(in srgb, var(--color-pink-500) 18%, transparent) 50%,
color-mix(in srgb, var(--color-pink-500) 7%, transparent) 66%,
transparent 100%
);
filter: blur(18px);
opacity: 0;
transform: translateX(-35%);
animation: startup-loader-scan 2.8s ease-in-out infinite;
animation-delay: -0.8s;
}
.startup-loader__brand {
display: flex;
align-items: center;
gap: 0.875rem;
}
.startup-loader__logo-wrap {
position: relative;
display: grid;
width: 3.25rem;
height: 3.25rem;
flex: none;
place-items: center;
}
.startup-loader__logo-wrap::before {
position: absolute;
inset: 0.375rem;
border-radius: 9999px;
background: color-mix(in srgb, var(--color-pink-500) 18%, transparent);
box-shadow: 0 0 26px color-mix(in srgb, var(--color-pink-500) 24%, transparent);
content: '';
filter: blur(8px);
animation: startup-loader-breathe 2.8s ease-in-out infinite;
animation-delay: -0.8s;
}
.startup-loader__logo {
position: relative;
width: 2.5rem;
height: 2.5rem;
user-select: none;
filter: drop-shadow(0 4px 10px color-mix(in srgb, var(--color-pink-500) 18%, transparent));
pointer-events: none;
}
.startup-loader__name {
animation: startup-loader-name 2.8s cubic-bezier(0.22, 1, 0.36, 1) infinite;
animation-delay: -0.8s;
font-size: 1.25rem;
font-weight: 800;
line-height: 1.2;
letter-spacing: -0.02em;
}
@keyframes startup-loader-name {
0%,
12% {
opacity: 0;
transform: translateX(1.75rem);
}
26%,
76% {
opacity: 1;
transform: translateX(0);
}
90%,
100% {
opacity: 0;
transform: translateX(-0.375rem);
}
}
@keyframes startup-loader-scan {
0%,
10% {
opacity: 0;
transform: translateX(-35%);
}
38%,
68% {
opacity: 1;
}
100% {
opacity: 0;
transform: translateX(35%);
}
}
@keyframes startup-loader-breathe {
0%,
100% {
opacity: 0.55;
transform: scale(0.88);
}
48% {
opacity: 1;
transform: scale(1.08);
}
}
@media (prefers-reduced-motion: reduce) {
.startup-loader__glow,
.startup-loader__logo-wrap::before,
.startup-loader__name {
animation: none;
}
.startup-loader__glow {
opacity: 0.6;
transform: none;
}
.startup-loader__name {
opacity: 1;
transform: none;
}
}
</style>
+1
View File
@@ -9,6 +9,7 @@ export { default as SectionCard } from './SectionCard.vue'
export { default as StatCard } from './StatCard.vue'
export { default as EmptyState } from './EmptyState.vue'
export { default as LoadingState } from './LoadingState.vue'
export { default as StartupLoading } from './StartupLoading.vue'
export { default as Tabs } from './Tabs.vue'
export { default as UITabs } from './Tabs.vue'
export { default as PageAnchorsNav } from './PageAnchorsNav.vue'