mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(ui): add branded startup loading animation
This commit is contained in:
1 parent
c6676e68a5
commit
ac64cb72fb
5 files changed
+178
-11
No files matched your search
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user