mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-02 15:52:26 +08:00
refactor(frontend): 优化公共组件和布局组件
- 更新 Logo 相关组件 (AetherLogo, HeaderLogo, RippleLogo 等) - 优化图表组件 (BarChart, LineChart, ScatterChart) - 改进公共组件 (AlertDialog, EmptyState, LoadingState) - 调整布局组件 (AppShell, SidebarNav, PageHeader 等) - 优化 ActivityHeatmap 统计组件
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="line-by-line-logo" :style="{ width: `${size}px`, height: `${size}px` }">
|
||||
<div
|
||||
class="line-by-line-logo"
|
||||
:style="{ width: `${size}px`, height: `${size}px` }"
|
||||
>
|
||||
<svg
|
||||
:viewBox="viewBox"
|
||||
class="logo-svg"
|
||||
@@ -7,12 +10,33 @@
|
||||
>
|
||||
<defs>
|
||||
<!-- Metallic gradient -->
|
||||
<linearGradient :id="gradientId" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" :stop-color="metallicColors.dark" />
|
||||
<stop offset="25%" :stop-color="metallicColors.base" />
|
||||
<stop offset="50%" :stop-color="metallicColors.light" />
|
||||
<stop offset="75%" :stop-color="metallicColors.base" />
|
||||
<stop offset="100%" :stop-color="metallicColors.dark" />
|
||||
<linearGradient
|
||||
:id="gradientId"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
:stop-color="metallicColors.dark"
|
||||
/>
|
||||
<stop
|
||||
offset="25%"
|
||||
:stop-color="metallicColors.base"
|
||||
/>
|
||||
<stop
|
||||
offset="50%"
|
||||
:stop-color="metallicColors.light"
|
||||
/>
|
||||
<stop
|
||||
offset="75%"
|
||||
:stop-color="metallicColors.base"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
:stop-color="metallicColors.dark"
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
@@ -73,6 +97,41 @@ interface ColorScheme {
|
||||
secondary: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
lineDelay?: number
|
||||
strokeDuration?: number
|
||||
fillDuration?: number
|
||||
autoStart?: boolean
|
||||
loop?: boolean
|
||||
loopPause?: number
|
||||
outlineColor?: string
|
||||
gradientColor?: string
|
||||
strokeWidth?: number
|
||||
cycleColors?: boolean
|
||||
isDark?: boolean
|
||||
}>(),
|
||||
{
|
||||
size: 400,
|
||||
lineDelay: 60,
|
||||
strokeDuration: 1200,
|
||||
fillDuration: 800,
|
||||
autoStart: true,
|
||||
loop: true,
|
||||
loopPause: 600,
|
||||
outlineColor: '#cc785c',
|
||||
gradientColor: '#e8a882',
|
||||
strokeWidth: 2.5,
|
||||
cycleColors: false,
|
||||
isDark: false
|
||||
}
|
||||
)
|
||||
const emit = defineEmits<{
|
||||
(e: 'animationComplete'): void
|
||||
(e: 'phaseChange', phase: AnimationPhase): void
|
||||
(e: 'colorChange', colors: ColorScheme): void
|
||||
}>()
|
||||
// Constants
|
||||
const LINE_COUNT = AETHER_LINE_PATHS.length
|
||||
const DEFAULT_PATH_LENGTH = 3000
|
||||
@@ -109,43 +168,6 @@ const DARK_MODE_SCHEMES: ColorScheme[] = [
|
||||
{ primary: '#e879f9', secondary: '#f5d0fe' },
|
||||
]
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
lineDelay?: number
|
||||
strokeDuration?: number
|
||||
fillDuration?: number
|
||||
autoStart?: boolean
|
||||
loop?: boolean
|
||||
loopPause?: number
|
||||
outlineColor?: string
|
||||
gradientColor?: string
|
||||
strokeWidth?: number
|
||||
cycleColors?: boolean
|
||||
isDark?: boolean
|
||||
}>(),
|
||||
{
|
||||
size: 400,
|
||||
lineDelay: 60,
|
||||
strokeDuration: 1200,
|
||||
fillDuration: 800,
|
||||
autoStart: true,
|
||||
loop: true,
|
||||
loopPause: 600,
|
||||
outlineColor: '#cc785c',
|
||||
gradientColor: '#e8a882',
|
||||
strokeWidth: 2.5,
|
||||
cycleColors: false,
|
||||
isDark: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'animationComplete'): void
|
||||
(e: 'phaseChange', phase: AnimationPhase): void
|
||||
(e: 'colorChange', colors: ColorScheme): void
|
||||
}>()
|
||||
|
||||
// Unique ID for gradient
|
||||
const gradientId = `aether-gradient-${Math.random().toString(36).slice(2, 9)}`
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<pre><code :class="`language-${language}`" v-html="highlightedCode"></code></pre>
|
||||
<pre><code
|
||||
:class="`language-${language}`"
|
||||
v-html="highlightedCode"
|
||||
/></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,6 +15,11 @@ import json from 'highlight.js/lib/languages/json'
|
||||
import ini from 'highlight.js/lib/languages/ini'
|
||||
import javascript from 'highlight.js/lib/languages/javascript'
|
||||
|
||||
const props = defineProps<{
|
||||
code: string
|
||||
language: string
|
||||
dense?: boolean
|
||||
}>()
|
||||
// 注册需要的语言
|
||||
hljs.registerLanguage('bash', bash)
|
||||
hljs.registerLanguage('sh', bash)
|
||||
@@ -20,12 +28,6 @@ hljs.registerLanguage('toml', ini)
|
||||
hljs.registerLanguage('ini', ini)
|
||||
hljs.registerLanguage('javascript', javascript)
|
||||
|
||||
const props = defineProps<{
|
||||
code: string
|
||||
language: string
|
||||
dense?: boolean
|
||||
}>()
|
||||
|
||||
const wrapperClass = computed(() =>
|
||||
['code-highlight', props.dense ? 'code-highlight--dense' : '']
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -5,34 +5,106 @@
|
||||
:class="{ 'scattering': isScattering, 'fading-out': isFadingOut }"
|
||||
>
|
||||
<!-- SVG Defs for the Gemini multi-color gradient -->
|
||||
<svg class="absolute w-0 h-0 overflow-hidden" aria-hidden="true">
|
||||
<svg
|
||||
class="absolute w-0 h-0 overflow-hidden"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<!-- Main Gemini gradient (blue base with color overlays) -->
|
||||
<linearGradient id="gemini-base" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#1A73E8" />
|
||||
<stop offset="50%" stop-color="#4285F4" />
|
||||
<stop offset="100%" stop-color="#669DF6" />
|
||||
<linearGradient
|
||||
id="gemini-base"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#1A73E8"
|
||||
/>
|
||||
<stop
|
||||
offset="50%"
|
||||
stop-color="#4285F4"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#669DF6"
|
||||
/>
|
||||
</linearGradient>
|
||||
<!-- Red accent overlay - from top -->
|
||||
<linearGradient id="gemini-red-overlay" x1="50%" y1="0%" x2="50%" y2="50%">
|
||||
<stop offset="0%" stop-color="#EA4335" />
|
||||
<stop offset="100%" stop-color="#EA4335" stop-opacity="0" />
|
||||
<linearGradient
|
||||
id="gemini-red-overlay"
|
||||
x1="50%"
|
||||
y1="0%"
|
||||
x2="50%"
|
||||
y2="50%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#EA4335"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#EA4335"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
<!-- Yellow accent overlay - from left -->
|
||||
<linearGradient id="gemini-yellow-overlay" x1="0%" y1="50%" x2="50%" y2="50%">
|
||||
<stop offset="0%" stop-color="#FBBC04" />
|
||||
<stop offset="100%" stop-color="#FBBC04" stop-opacity="0" />
|
||||
<linearGradient
|
||||
id="gemini-yellow-overlay"
|
||||
x1="0%"
|
||||
y1="50%"
|
||||
x2="50%"
|
||||
y2="50%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#FBBC04"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#FBBC04"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
<!-- Green accent overlay - from bottom -->
|
||||
<linearGradient id="gemini-green-overlay" x1="50%" y1="100%" x2="50%" y2="50%">
|
||||
<stop offset="0%" stop-color="#34A853" />
|
||||
<stop offset="100%" stop-color="#34A853" stop-opacity="0" />
|
||||
<linearGradient
|
||||
id="gemini-green-overlay"
|
||||
x1="50%"
|
||||
y1="100%"
|
||||
x2="50%"
|
||||
y2="50%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#34A853"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#34A853"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
<!-- Glow filter -->
|
||||
<filter id="star-glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||
<feFlood flood-color="#4285F4" flood-opacity="0.3" />
|
||||
<feComposite in2="blur" operator="in" />
|
||||
<filter
|
||||
id="star-glow"
|
||||
x="-50%"
|
||||
y="-50%"
|
||||
width="200%"
|
||||
height="200%"
|
||||
>
|
||||
<feGaussianBlur
|
||||
stdDeviation="2"
|
||||
result="blur"
|
||||
/>
|
||||
<feFlood
|
||||
flood-color="#4285F4"
|
||||
flood-opacity="0.3"
|
||||
/>
|
||||
<feComposite
|
||||
in2="blur"
|
||||
operator="in"
|
||||
/>
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
@@ -50,11 +122,26 @@
|
||||
:class="{ 'star-visible': star.visible && hasScattered }"
|
||||
:style="getStarStyle(star)"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="star-svg">
|
||||
<path :d="starPath" fill="url(#gemini-base)" />
|
||||
<path :d="starPath" fill="url(#gemini-red-overlay)" />
|
||||
<path :d="starPath" fill="url(#gemini-yellow-overlay)" />
|
||||
<path :d="starPath" fill="url(#gemini-green-overlay)" />
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
class="star-svg"
|
||||
>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-base)"
|
||||
/>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-red-overlay)"
|
||||
/>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-yellow-overlay)"
|
||||
/>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-green-overlay)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,11 +155,27 @@
|
||||
:class="{ 'star-visible': star.visible && hasScattered }"
|
||||
:style="getStarStyle(star)"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="star-svg" style="filter: url(#star-glow)">
|
||||
<path :d="starPath" fill="url(#gemini-base)" />
|
||||
<path :d="starPath" fill="url(#gemini-red-overlay)" />
|
||||
<path :d="starPath" fill="url(#gemini-yellow-overlay)" />
|
||||
<path :d="starPath" fill="url(#gemini-green-overlay)" />
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
class="star-svg"
|
||||
style="filter: url(#star-glow)"
|
||||
>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-base)"
|
||||
/>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-red-overlay)"
|
||||
/>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-yellow-overlay)"
|
||||
/>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-green-overlay)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,11 +189,27 @@
|
||||
:class="{ 'star-visible': star.visible && hasScattered }"
|
||||
:style="getStarStyle(star)"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="star-svg" style="filter: url(#star-glow)">
|
||||
<path :d="starPath" fill="url(#gemini-base)" />
|
||||
<path :d="starPath" fill="url(#gemini-red-overlay)" />
|
||||
<path :d="starPath" fill="url(#gemini-yellow-overlay)" />
|
||||
<path :d="starPath" fill="url(#gemini-green-overlay)" />
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
class="star-svg"
|
||||
style="filter: url(#star-glow)"
|
||||
>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-base)"
|
||||
/>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-red-overlay)"
|
||||
/>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-yellow-overlay)"
|
||||
/>
|
||||
<path
|
||||
:d="starPath"
|
||||
fill="url(#gemini-green-overlay)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 799.31 752.14"
|
||||
class="transition-colors duration-500 ease-out"
|
||||
:class="[
|
||||
'transition-colors duration-500 ease-out',
|
||||
logoClass
|
||||
]"
|
||||
>
|
||||
@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
className: ''
|
||||
})
|
||||
|
||||
const containerClass = props.size + ' ' + props.className
|
||||
const containerClass = `${props.size } ${ props.className}`
|
||||
const logoClass = 'w-full h-full'
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,16 +10,26 @@
|
||||
@keydown.escape.stop="closeDropdown"
|
||||
>
|
||||
<div class="platform-select__current">
|
||||
<component :is="currentOption.icon" class="platform-select__icon" />
|
||||
<component
|
||||
:is="currentOption.icon"
|
||||
class="platform-select__icon"
|
||||
/>
|
||||
<div class="platform-select__text">
|
||||
<p class="platform-select__label">{{ currentOption.label }}</p>
|
||||
<p class="platform-select__hint">{{ currentOption.hint }}</p>
|
||||
<p class="platform-select__label">
|
||||
{{ currentOption.label }}
|
||||
</p>
|
||||
<p class="platform-select__hint">
|
||||
{{ currentOption.hint }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown class="platform-select__chevron" />
|
||||
|
||||
<transition name="platform-select-fade">
|
||||
<ul v-if="isOpen" class="platform-select__dropdown">
|
||||
<ul
|
||||
v-if="isOpen"
|
||||
class="platform-select__dropdown"
|
||||
>
|
||||
<li
|
||||
v-for="option in resolvedOptions"
|
||||
:key="option.value"
|
||||
@@ -27,12 +37,22 @@
|
||||
:class="{ 'platform-select__option--active': option.value === modelValue }"
|
||||
@click.stop="selectOption(option.value)"
|
||||
>
|
||||
<component :is="option.icon" class="platform-select__option-icon" />
|
||||
<component
|
||||
:is="option.icon"
|
||||
class="platform-select__option-icon"
|
||||
/>
|
||||
<div class="platform-select__option-copy">
|
||||
<p class="platform-select__option-label">{{ option.label }}</p>
|
||||
<p class="platform-select__option-hint">{{ option.hint }}</p>
|
||||
<p class="platform-select__option-label">
|
||||
{{ option.label }}
|
||||
</p>
|
||||
<p class="platform-select__option-hint">
|
||||
{{ option.hint }}
|
||||
</p>
|
||||
</div>
|
||||
<Check class="platform-select__option-check" v-if="option.value === modelValue" />
|
||||
<Check
|
||||
v-if="option.value === modelValue"
|
||||
class="platform-select__option-check"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
|
||||
@@ -1,355 +1,599 @@
|
||||
<template>
|
||||
<Transition name="logo-fade">
|
||||
<!-- Adaptive Aether logo using external SVG with CSS-based dark mode -->
|
||||
<!-- Animation sequence: stroke outline -> fill color -> ripple breathing -->
|
||||
<div
|
||||
v-if="type === 'aether' && useAdaptive"
|
||||
:key="`aether-adaptive-${animationKey}`"
|
||||
class="aether-adaptive-container"
|
||||
:style="{ '--anim-delay': `${animDelay}ms` }"
|
||||
>
|
||||
<!-- Definitions for gradient and glow -->
|
||||
<svg style="position: absolute; width: 0; height: 0; overflow: hidden;" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="adaptive-aether-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#cc785c" />
|
||||
<stop offset="50%" stop-color="#d4a27f" />
|
||||
<stop offset="100%" stop-color="#cc785c" />
|
||||
</linearGradient>
|
||||
<filter id="adaptive-aether-glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<!-- Ripple layers - start after fill completes -->
|
||||
<div class="adaptive-ripple r-1" :class="{ active: adaptiveFillComplete }">
|
||||
<svg viewBox="0 0 799.31 752.14" class="adaptive-logo-img">
|
||||
<path :d="adaptiveAetherPath" fill="none" stroke="url(#adaptive-aether-gradient)" stroke-width="2" vector-effect="non-scaling-stroke" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="adaptive-ripple r-2" :class="{ active: adaptiveFillComplete }">
|
||||
<svg viewBox="0 0 799.31 752.14" class="adaptive-logo-img">
|
||||
<path :d="adaptiveAetherPath" fill="none" stroke="url(#adaptive-aether-gradient)" stroke-width="2" vector-effect="non-scaling-stroke" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="adaptive-ripple r-3" :class="{ active: adaptiveFillComplete }">
|
||||
<svg viewBox="0 0 799.31 752.14" class="adaptive-logo-img">
|
||||
<path :d="adaptiveAetherPath" fill="none" stroke="url(#adaptive-aether-gradient)" stroke-width="2" vector-effect="non-scaling-stroke" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Phase 1: Stroke outline drawing (SVG overlay) -->
|
||||
<svg
|
||||
class="adaptive-stroke-overlay"
|
||||
:class="{ 'stroke-complete': adaptiveStrokeComplete }"
|
||||
viewBox="0 0 799.31 752.14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class="adaptive-stroke-path"
|
||||
:d="adaptiveAetherPath"
|
||||
style="stroke: url(#adaptive-aether-gradient); filter: url(#adaptive-aether-glow);"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Phase 2: Fill using SVG path -->
|
||||
<div
|
||||
class="adaptive-fill-layer"
|
||||
:class="{ 'fill-active': adaptiveStrokeComplete, 'fill-complete': adaptiveFillComplete, 'breathing': adaptiveFillComplete }"
|
||||
>
|
||||
<svg viewBox="0 0 799.31 752.14" class="adaptive-fill-img">
|
||||
<path :d="adaptiveAetherPath" fill="url(#adaptive-aether-gradient)" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aether logo: single complex path with ripple effect -->
|
||||
<Transition name="logo-fade">
|
||||
<!-- Adaptive Aether logo using external SVG with CSS-based dark mode -->
|
||||
<!-- Animation sequence: stroke outline -> fill color -> ripple breathing -->
|
||||
<div
|
||||
v-if="type === 'aether' && useAdaptive"
|
||||
:key="`aether-adaptive-${animationKey}`"
|
||||
class="aether-adaptive-container"
|
||||
:style="{ '--anim-delay': `${animDelay}ms` }"
|
||||
>
|
||||
<!-- Definitions for gradient and glow -->
|
||||
<svg
|
||||
v-else-if="type === 'aether'"
|
||||
:key="`aether-${animationKey}`"
|
||||
:viewBox="viewBox"
|
||||
class="ripple-logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:style="{ '--anim-delay': `${animDelay}ms` }"
|
||||
style="position: absolute; width: 0; height: 0; overflow: hidden;"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<path id="aether-path" :d="aetherPath" ref="aetherPathRef" />
|
||||
<!-- Gradient for breathing glow effect -->
|
||||
<linearGradient id="aether-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#cc785c" />
|
||||
<stop offset="50%" stop-color="#d4a27f" />
|
||||
<stop offset="100%" stop-color="#cc785c" />
|
||||
<linearGradient
|
||||
id="adaptive-aether-gradient"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#cc785c"
|
||||
/>
|
||||
<stop
|
||||
offset="50%"
|
||||
stop-color="#d4a27f"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#cc785c"
|
||||
/>
|
||||
</linearGradient>
|
||||
<!-- Glow filter for breathing effect -->
|
||||
<filter id="aether-glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="4" result="coloredBlur" />
|
||||
<filter
|
||||
id="adaptive-aether-glow"
|
||||
x="-50%"
|
||||
y="-50%"
|
||||
width="200%"
|
||||
height="200%"
|
||||
>
|
||||
<feGaussianBlur
|
||||
stdDeviation="3"
|
||||
result="coloredBlur"
|
||||
/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Static mode: just show filled logo with fade-in animation -->
|
||||
<template v-if="static">
|
||||
<use
|
||||
href="#aether-path"
|
||||
class="static-fill"
|
||||
:style="{ fill: strokeColor }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Animated mode -->
|
||||
<template v-else>
|
||||
<!-- Main logo - with stroke drawing animation -->
|
||||
<use
|
||||
href="#aether-path"
|
||||
class="fine-line stroke-draw aether-stroke"
|
||||
:class="{ 'draw-complete': drawComplete, 'breathing': drawComplete && !disableRipple }"
|
||||
:style="{ stroke: drawComplete ? 'url(#aether-gradient)' : strokeColor, '--path-length': aetherPathLength, transformOrigin: aetherCenter }"
|
||||
:filter="drawComplete ? 'url(#aether-glow)' : 'none'"
|
||||
/>
|
||||
<!-- Main logo - fill (fade in after draw) -->
|
||||
<use
|
||||
href="#aether-path"
|
||||
class="aether-fill"
|
||||
:class="{ 'fill-active': drawComplete, 'breathing': drawComplete && !disableRipple }"
|
||||
:style="{ fill: strokeColor, transformOrigin: aetherCenter }"
|
||||
/>
|
||||
<use
|
||||
v-if="!disableRipple"
|
||||
href="#aether-path"
|
||||
class="fine-line ripple d-1"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: aetherCenter }"
|
||||
/>
|
||||
<use
|
||||
v-if="!disableRipple"
|
||||
href="#aether-path"
|
||||
class="fine-line ripple d-2"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: aetherCenter }"
|
||||
/>
|
||||
<use
|
||||
v-if="!disableRipple"
|
||||
href="#aether-path"
|
||||
class="fine-line ripple d-3"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: aetherCenter }"
|
||||
/>
|
||||
</template>
|
||||
</svg>
|
||||
|
||||
<!-- Standard single-path logos -->
|
||||
<svg
|
||||
v-else
|
||||
:key="`${type}-${animationKey}`"
|
||||
:viewBox="viewBox"
|
||||
class="ripple-logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:style="{ '--anim-delay': `${animDelay}ms` }"
|
||||
>
|
||||
<defs>
|
||||
<path :id="pathId" :d="pathData" />
|
||||
<!-- Gemini multi-layer gradients -->
|
||||
<template v-if="type === 'gemini'">
|
||||
<!-- Fill gradients -->
|
||||
<linearGradient gradientUnits="userSpaceOnUse" :id="`${pathId}-fill-0`" x1="7" x2="11" y1="15.5" y2="12">
|
||||
<stop stop-color="#08B962"></stop>
|
||||
<stop offset="1" stop-color="#08B962" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" :id="`${pathId}-fill-1`" x1="8" x2="11.5" y1="5.5" y2="11">
|
||||
<stop stop-color="#F94543"></stop>
|
||||
<stop offset="1" stop-color="#F94543" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" :id="`${pathId}-fill-2`" x1="3.5" x2="17.5" y1="13.5" y2="12">
|
||||
<stop stop-color="#FABC12"></stop>
|
||||
<stop offset=".46" stop-color="#FABC12" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
<!-- Stroke gradient for outline - 4 directional gradients to match logo colors -->
|
||||
<!-- Top point = red, Right point = blue, Bottom point = green, Left point = yellow -->
|
||||
<linearGradient gradientUnits="userSpaceOnUse" :id="`${pathId}-stroke-v`" x1="12" x2="12" y1="1" y2="23">
|
||||
<stop offset="0%" stop-color="#F94543" />
|
||||
<stop offset="50%" stop-color="#3186FF" />
|
||||
<stop offset="100%" stop-color="#08B962" />
|
||||
</linearGradient>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" :id="`${pathId}-stroke-h`" x1="1" x2="23" y1="12" y2="12">
|
||||
<stop offset="0%" stop-color="#FABC12" />
|
||||
<stop offset="50%" stop-color="#3186FF" />
|
||||
<stop offset="100%" stop-color="#3186FF" />
|
||||
</linearGradient>
|
||||
<!-- Mask for fill-inward animation (controlled by JS) -->
|
||||
<mask :id="`${pathId}-fill-mask`">
|
||||
<rect x="-4" y="-4" width="32" height="32" fill="white" />
|
||||
<circle cx="12" cy="12" :r="geminiFillRadius" fill="black" />
|
||||
</mask>
|
||||
</template>
|
||||
</defs>
|
||||
|
||||
<!-- OpenAI special rendering: stroke outline -> fill -> rotate + breathe -->
|
||||
<template v-if="type === 'openai'">
|
||||
<!-- Outer breathing wrapper (scale pulse) -->
|
||||
<!-- Ripple layers - start after fill completes -->
|
||||
<div
|
||||
class="adaptive-ripple r-1"
|
||||
:class="{ active: adaptiveFillComplete }"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 799.31 752.14"
|
||||
class="adaptive-logo-img"
|
||||
>
|
||||
<path
|
||||
:d="adaptiveAetherPath"
|
||||
fill="none"
|
||||
stroke="url(#adaptive-aether-gradient)"
|
||||
stroke-width="2"
|
||||
vector-effect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="adaptive-ripple r-2"
|
||||
:class="{ active: adaptiveFillComplete }"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 799.31 752.14"
|
||||
class="adaptive-logo-img"
|
||||
>
|
||||
<path
|
||||
:d="adaptiveAetherPath"
|
||||
fill="none"
|
||||
stroke="url(#adaptive-aether-gradient)"
|
||||
stroke-width="2"
|
||||
vector-effect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="adaptive-ripple r-3"
|
||||
:class="{ active: adaptiveFillComplete }"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 799.31 752.14"
|
||||
class="adaptive-logo-img"
|
||||
>
|
||||
<path
|
||||
:d="adaptiveAetherPath"
|
||||
fill="none"
|
||||
stroke="url(#adaptive-aether-gradient)"
|
||||
stroke-width="2"
|
||||
vector-effect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Phase 1: Stroke outline drawing (SVG overlay) -->
|
||||
<svg
|
||||
class="adaptive-stroke-overlay"
|
||||
:class="{ 'stroke-complete': adaptiveStrokeComplete }"
|
||||
viewBox="0 0 799.31 752.14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class="adaptive-stroke-path"
|
||||
:d="adaptiveAetherPath"
|
||||
style="stroke: url(#adaptive-aether-gradient); filter: url(#adaptive-aether-glow);"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Phase 2: Fill using SVG path -->
|
||||
<div
|
||||
class="adaptive-fill-layer"
|
||||
:class="{ 'fill-active': adaptiveStrokeComplete, 'fill-complete': adaptiveFillComplete, 'breathing': adaptiveFillComplete }"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 799.31 752.14"
|
||||
class="adaptive-fill-img"
|
||||
>
|
||||
<path
|
||||
:d="adaptiveAetherPath"
|
||||
fill="url(#adaptive-aether-gradient)"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aether logo: single complex path with ripple effect -->
|
||||
<svg
|
||||
v-else-if="type === 'aether'"
|
||||
:key="`aether-${animationKey}`"
|
||||
:viewBox="viewBox"
|
||||
class="ripple-logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:style="{ '--anim-delay': `${animDelay}ms` }"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
id="aether-path"
|
||||
ref="aetherPathRef"
|
||||
:d="aetherPath"
|
||||
/>
|
||||
<!-- Gradient for breathing glow effect -->
|
||||
<linearGradient
|
||||
id="aether-gradient"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#cc785c"
|
||||
/>
|
||||
<stop
|
||||
offset="50%"
|
||||
stop-color="#d4a27f"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#cc785c"
|
||||
/>
|
||||
</linearGradient>
|
||||
<!-- Glow filter for breathing effect -->
|
||||
<filter
|
||||
id="aether-glow"
|
||||
x="-50%"
|
||||
y="-50%"
|
||||
width="200%"
|
||||
height="200%"
|
||||
>
|
||||
<feGaussianBlur
|
||||
stdDeviation="4"
|
||||
result="coloredBlur"
|
||||
/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Static mode: just show filled logo with fade-in animation -->
|
||||
<template v-if="static">
|
||||
<use
|
||||
href="#aether-path"
|
||||
class="static-fill"
|
||||
:style="{ fill: strokeColor }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Animated mode -->
|
||||
<template v-else>
|
||||
<!-- Main logo - with stroke drawing animation -->
|
||||
<use
|
||||
href="#aether-path"
|
||||
class="fine-line stroke-draw aether-stroke"
|
||||
:class="{ 'draw-complete': drawComplete, 'breathing': drawComplete && !disableRipple }"
|
||||
:style="{ stroke: drawComplete ? 'url(#aether-gradient)' : strokeColor, '--path-length': aetherPathLength, transformOrigin: aetherCenter }"
|
||||
:filter="drawComplete ? 'url(#aether-glow)' : 'none'"
|
||||
/>
|
||||
<!-- Main logo - fill (fade in after draw) -->
|
||||
<use
|
||||
href="#aether-path"
|
||||
class="aether-fill"
|
||||
:class="{ 'fill-active': drawComplete, 'breathing': drawComplete && !disableRipple }"
|
||||
:style="{ fill: strokeColor, transformOrigin: aetherCenter }"
|
||||
/>
|
||||
<use
|
||||
v-if="!disableRipple"
|
||||
href="#aether-path"
|
||||
class="fine-line ripple d-1"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: aetherCenter }"
|
||||
/>
|
||||
<use
|
||||
v-if="!disableRipple"
|
||||
href="#aether-path"
|
||||
class="fine-line ripple d-2"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: aetherCenter }"
|
||||
/>
|
||||
<use
|
||||
v-if="!disableRipple"
|
||||
href="#aether-path"
|
||||
class="fine-line ripple d-3"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: aetherCenter }"
|
||||
/>
|
||||
</template>
|
||||
</svg>
|
||||
|
||||
<!-- Standard single-path logos -->
|
||||
<svg
|
||||
v-else
|
||||
:key="`${type}-${animationKey}`"
|
||||
:viewBox="viewBox"
|
||||
class="ripple-logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:style="{ '--anim-delay': `${animDelay}ms` }"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
:id="pathId"
|
||||
:d="pathData"
|
||||
/>
|
||||
<!-- Gemini multi-layer gradients -->
|
||||
<template v-if="type === 'gemini'">
|
||||
<!-- Fill gradients -->
|
||||
<linearGradient
|
||||
:id="`${pathId}-fill-0`"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="7"
|
||||
x2="11"
|
||||
y1="15.5"
|
||||
y2="12"
|
||||
>
|
||||
<stop stop-color="#08B962" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#08B962"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
:id="`${pathId}-fill-1`"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="8"
|
||||
x2="11.5"
|
||||
y1="5.5"
|
||||
y2="11"
|
||||
>
|
||||
<stop stop-color="#F94543" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#F94543"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
:id="`${pathId}-fill-2`"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="3.5"
|
||||
x2="17.5"
|
||||
y1="13.5"
|
||||
y2="12"
|
||||
>
|
||||
<stop stop-color="#FABC12" />
|
||||
<stop
|
||||
offset=".46"
|
||||
stop-color="#FABC12"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
<!-- Stroke gradient for outline - 4 directional gradients to match logo colors -->
|
||||
<!-- Top point = red, Right point = blue, Bottom point = green, Left point = yellow -->
|
||||
<linearGradient
|
||||
:id="`${pathId}-stroke-v`"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="1"
|
||||
y2="23"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#F94543"
|
||||
/>
|
||||
<stop
|
||||
offset="50%"
|
||||
stop-color="#3186FF"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#08B962"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
:id="`${pathId}-stroke-h`"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="1"
|
||||
x2="23"
|
||||
y1="12"
|
||||
y2="12"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#FABC12"
|
||||
/>
|
||||
<stop
|
||||
offset="50%"
|
||||
stop-color="#3186FF"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#3186FF"
|
||||
/>
|
||||
</linearGradient>
|
||||
<!-- Mask for fill-inward animation (controlled by JS) -->
|
||||
<mask :id="`${pathId}-fill-mask`">
|
||||
<rect
|
||||
x="-4"
|
||||
y="-4"
|
||||
width="32"
|
||||
height="32"
|
||||
fill="white"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
:r="geminiFillRadius"
|
||||
fill="black"
|
||||
/>
|
||||
</mask>
|
||||
</template>
|
||||
</defs>
|
||||
|
||||
<!-- OpenAI special rendering: stroke outline -> fill -> rotate + breathe -->
|
||||
<template v-if="type === 'openai'">
|
||||
<!-- Outer breathing wrapper (scale pulse) -->
|
||||
<g
|
||||
class="openai-breathe-group"
|
||||
:class="{ 'breathing': drawComplete }"
|
||||
:style="{ transformOrigin: transformOrigin }"
|
||||
>
|
||||
<!-- Inner rotation wrapper -->
|
||||
<g
|
||||
class="openai-breathe-group"
|
||||
:class="{ 'breathing': drawComplete }"
|
||||
class="openai-rotate-group"
|
||||
:class="{ 'rotating': drawComplete }"
|
||||
:style="{ transformOrigin: transformOrigin }"
|
||||
>
|
||||
<!-- Inner rotation wrapper -->
|
||||
<g
|
||||
class="openai-rotate-group"
|
||||
:class="{ 'rotating': drawComplete }"
|
||||
:style="{ transformOrigin: transformOrigin }"
|
||||
>
|
||||
<!-- Step 1: Stroke outline drawing -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="openai-outline"
|
||||
:class="{ 'outline-complete': drawComplete }"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<!-- Step 2: Fill layer (appears after outline) -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="openai-fill"
|
||||
:class="{ 'fill-active': drawComplete }"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</g>
|
||||
<!-- Step 1: Stroke outline drawing -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="openai-outline"
|
||||
:class="{ 'outline-complete': drawComplete }"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<!-- Step 2: Fill layer (appears after outline) -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="openai-fill"
|
||||
:class="{ 'fill-active': drawComplete }"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</g>
|
||||
</template>
|
||||
</g>
|
||||
</template>
|
||||
|
||||
<!-- Claude special rendering: stroke outline -> fill -> ripple -->
|
||||
<template v-else-if="type === 'claude'">
|
||||
<!-- Step 1: Stroke outline drawing -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="claude-outline"
|
||||
:class="{ 'outline-complete': drawComplete }"
|
||||
stroke="#D97757"
|
||||
/>
|
||||
<!-- Step 2: Fill layer (appears after outline) -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="claude-fill"
|
||||
:class="{ 'fill-active': drawComplete }"
|
||||
fill="#D97757"
|
||||
/>
|
||||
<!-- Step 3: Ripple waves (after fill complete) -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line claude-ripple d-1"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: '#D97757', transformOrigin: transformOrigin }"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line claude-ripple d-2"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: '#D97757', transformOrigin: transformOrigin }"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line claude-ripple d-3"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: '#D97757', transformOrigin: transformOrigin }"
|
||||
/>
|
||||
</template>
|
||||
<!-- Claude special rendering: stroke outline -> fill -> ripple -->
|
||||
<template v-else-if="type === 'claude'">
|
||||
<!-- Step 1: Stroke outline drawing -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="claude-outline"
|
||||
:class="{ 'outline-complete': drawComplete }"
|
||||
stroke="#D97757"
|
||||
/>
|
||||
<!-- Step 2: Fill layer (appears after outline) -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="claude-fill"
|
||||
:class="{ 'fill-active': drawComplete }"
|
||||
fill="#D97757"
|
||||
/>
|
||||
<!-- Step 3: Ripple waves (after fill complete) -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line claude-ripple d-1"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: '#D97757', transformOrigin: transformOrigin }"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line claude-ripple d-2"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: '#D97757', transformOrigin: transformOrigin }"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line claude-ripple d-3"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: '#D97757', transformOrigin: transformOrigin }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Gemini special rendering: stroke outline -> fill -> breathe -->
|
||||
<template v-else-if="type === 'gemini'">
|
||||
<!-- Step 1: Stroke outline drawing (multi-layer colorful) -->
|
||||
<g class="gemini-outline-group" :class="{ 'outline-complete': drawComplete }">
|
||||
<use :href="`#${pathId}`" class="gemini-outline" stroke="#3186FF" />
|
||||
<use :href="`#${pathId}`" class="gemini-outline" :style="{ stroke: `url(#${pathId}-fill-0)` }" />
|
||||
<use :href="`#${pathId}`" class="gemini-outline" :style="{ stroke: `url(#${pathId}-fill-1)` }" />
|
||||
<use :href="`#${pathId}`" class="gemini-outline" :style="{ stroke: `url(#${pathId}-fill-2)` }" />
|
||||
</g>
|
||||
<!-- Step 2: Fill layer (appears after outline, with inward fill animation) -->
|
||||
<!-- Gemini special rendering: stroke outline -> fill -> breathe -->
|
||||
<template v-else-if="type === 'gemini'">
|
||||
<!-- Step 1: Stroke outline drawing (multi-layer colorful) -->
|
||||
<g
|
||||
class="gemini-outline-group"
|
||||
:class="{ 'outline-complete': drawComplete }"
|
||||
>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="gemini-outline"
|
||||
stroke="#3186FF"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="gemini-outline"
|
||||
:style="{ stroke: `url(#${pathId}-fill-0)` }"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="gemini-outline"
|
||||
:style="{ stroke: `url(#${pathId}-fill-1)` }"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="gemini-outline"
|
||||
:style="{ stroke: `url(#${pathId}-fill-2)` }"
|
||||
/>
|
||||
</g>
|
||||
<!-- Step 2: Fill layer (appears after outline, with inward fill animation) -->
|
||||
<g
|
||||
class="gemini-fill"
|
||||
:class="{ 'fill-complete': fillComplete }"
|
||||
:mask="`url(#${pathId}-fill-mask)`"
|
||||
>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
fill="#3186FF"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-0)`"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-1)`"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-2)`"
|
||||
/>
|
||||
</g>
|
||||
<!-- Step 3: Ripple waves (after fill complete) -->
|
||||
<g v-if="!disableRipple">
|
||||
<g
|
||||
class="gemini-fill"
|
||||
:class="{ 'fill-complete': fillComplete }"
|
||||
:mask="`url(#${pathId}-fill-mask)`"
|
||||
class="gemini-ripple d-1"
|
||||
:class="{ 'ripple-active': fillComplete }"
|
||||
:style="{ transformOrigin: transformOrigin }"
|
||||
>
|
||||
<use :href="`#${pathId}`" fill="#3186FF" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-0)`" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-1)`" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-2)`" />
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
fill="#3186FF"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-0)`"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-1)`"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-2)`"
|
||||
/>
|
||||
</g>
|
||||
<!-- Step 3: Ripple waves (after fill complete) -->
|
||||
<g v-if="!disableRipple">
|
||||
<g
|
||||
class="gemini-ripple d-1"
|
||||
:class="{ 'ripple-active': fillComplete }"
|
||||
:style="{ transformOrigin: transformOrigin }"
|
||||
>
|
||||
<use :href="`#${pathId}`" fill="#3186FF" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-0)`" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-1)`" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-2)`" />
|
||||
</g>
|
||||
<g
|
||||
class="gemini-ripple d-2"
|
||||
:class="{ 'ripple-active': fillComplete }"
|
||||
:style="{ transformOrigin: transformOrigin }"
|
||||
>
|
||||
<use :href="`#${pathId}`" fill="#3186FF" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-0)`" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-1)`" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-2)`" />
|
||||
</g>
|
||||
<g
|
||||
class="gemini-ripple d-3"
|
||||
:class="{ 'ripple-active': fillComplete }"
|
||||
:style="{ transformOrigin: transformOrigin }"
|
||||
>
|
||||
<use :href="`#${pathId}`" fill="#3186FF" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-0)`" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-1)`" />
|
||||
<use :href="`#${pathId}`" :fill="`url(#${pathId}-fill-2)`" />
|
||||
</g>
|
||||
<g
|
||||
class="gemini-ripple d-2"
|
||||
:class="{ 'ripple-active': fillComplete }"
|
||||
:style="{ transformOrigin: transformOrigin }"
|
||||
>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
fill="#3186FF"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-0)`"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-1)`"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-2)`"
|
||||
/>
|
||||
</g>
|
||||
</template>
|
||||
<g
|
||||
class="gemini-ripple d-3"
|
||||
:class="{ 'ripple-active': fillComplete }"
|
||||
:style="{ transformOrigin: transformOrigin }"
|
||||
>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
fill="#3186FF"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-0)`"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-1)`"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
:fill="`url(#${pathId}-fill-2)`"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</template>
|
||||
|
||||
<!-- Other logos: stroke-based rendering -->
|
||||
<template v-else>
|
||||
<!-- Static center icon with stroke drawing animation -->
|
||||
<!-- Other logos: stroke-based rendering -->
|
||||
<template v-else>
|
||||
<!-- Static center icon with stroke drawing animation -->
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line stroke-draw"
|
||||
:class="{ 'draw-complete': drawComplete }"
|
||||
:style="{ stroke: strokeColor, '--path-length': pathLength }"
|
||||
/>
|
||||
|
||||
<!-- Ripple waves - only active after drawing completes -->
|
||||
<g>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line stroke-draw"
|
||||
:class="{ 'draw-complete': drawComplete }"
|
||||
:style="{ stroke: strokeColor, '--path-length': pathLength }"
|
||||
class="fine-line ripple d-1"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: transformOrigin }"
|
||||
/>
|
||||
|
||||
<!-- Ripple waves - only active after drawing completes -->
|
||||
<g>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line ripple d-1"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: transformOrigin }"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line ripple d-2"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: transformOrigin }"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line ripple d-3"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: transformOrigin }"
|
||||
/>
|
||||
</g>
|
||||
</template>
|
||||
</svg>
|
||||
</Transition>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line ripple d-2"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: transformOrigin }"
|
||||
/>
|
||||
<use
|
||||
:href="`#${pathId}`"
|
||||
class="fine-line ripple d-3"
|
||||
:class="{ 'ripple-active': drawComplete }"
|
||||
:style="{ stroke: strokeColor, transformOrigin: transformOrigin }"
|
||||
/>
|
||||
</g>
|
||||
</template>
|
||||
</svg>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
@@ -35,28 +35,42 @@
|
||||
/>
|
||||
</svg>
|
||||
<!-- 图标 -->
|
||||
<div class="absolute inset-0 flex items-center justify-center" :class="iconClasses">
|
||||
<component :is="icon" class="w-4 h-4" />
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
:class="iconClasses"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p v-if="toast.title" class="text-sm font-medium" :class="titleClasses">
|
||||
<p
|
||||
v-if="toast.title"
|
||||
class="text-sm font-medium"
|
||||
:class="titleClasses"
|
||||
>
|
||||
{{ toast.title }}
|
||||
</p>
|
||||
<p v-if="toast.message" class="text-sm" :class="messageClasses">
|
||||
<p
|
||||
v-if="toast.message"
|
||||
class="text-sm"
|
||||
:class="messageClasses"
|
||||
>
|
||||
{{ toast.message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<button
|
||||
@click="$emit('remove')"
|
||||
class="shrink-0 p-1 rounded transition-colors opacity-40 hover:opacity-100"
|
||||
:class="closeClasses"
|
||||
type="button"
|
||||
aria-label="关闭"
|
||||
@click="$emit('remove')"
|
||||
>
|
||||
<X class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<canvas ref="chartRef"></canvas>
|
||||
<canvas ref="chartRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,6 +19,11 @@ import {
|
||||
type ChartOptions
|
||||
} from 'chart.js'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 300,
|
||||
stacked: true
|
||||
})
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
@@ -36,11 +41,6 @@ interface Props {
|
||||
stacked?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 300,
|
||||
stacked: true
|
||||
})
|
||||
|
||||
const chartRef = ref<HTMLCanvasElement>()
|
||||
let chart: ChartJS<'bar'> | null = null
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<canvas ref="chartRef"></canvas>
|
||||
<canvas ref="chartRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
type ChartOptions
|
||||
} from 'chart.js'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 300
|
||||
})
|
||||
|
||||
// 注册 Chart.js 组件
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
@@ -38,10 +42,6 @@ interface Props {
|
||||
height?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 300
|
||||
})
|
||||
|
||||
const chartRef = ref<HTMLCanvasElement>()
|
||||
let chart: ChartJS<'line'> | null = null
|
||||
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
<template>
|
||||
<div class="w-full h-full relative">
|
||||
<canvas ref="chartRef"></canvas>
|
||||
<canvas ref="chartRef" />
|
||||
<div
|
||||
v-if="crosshairStats"
|
||||
class="absolute top-2 right-2 bg-gray-800/90 text-gray-100 px-3 py-2 rounded-lg text-sm shadow-lg border border-gray-600"
|
||||
>
|
||||
<div class="font-medium text-yellow-400">Y = {{ crosshairStats.yValue.toFixed(1) }} 分钟</div>
|
||||
<div class="font-medium text-yellow-400">
|
||||
Y = {{ crosshairStats.yValue.toFixed(1) }} 分钟
|
||||
</div>
|
||||
<!-- 单个 dataset 时显示简单统计 -->
|
||||
<div v-if="crosshairStats.datasets.length === 1" class="mt-1">
|
||||
<div
|
||||
v-if="crosshairStats.datasets.length === 1"
|
||||
class="mt-1"
|
||||
>
|
||||
<span class="text-green-400">{{ crosshairStats.datasets[0].belowCount }}</span> / {{ crosshairStats.datasets[0].totalCount }} 点在横线以下
|
||||
<span class="ml-2 text-blue-400">({{ crosshairStats.datasets[0].belowPercent.toFixed(1) }}%)</span>
|
||||
</div>
|
||||
<!-- 多个 dataset 时按模型分别显示 -->
|
||||
<div v-else class="mt-1 space-y-0.5">
|
||||
<div
|
||||
v-else
|
||||
class="mt-1 space-y-0.5"
|
||||
>
|
||||
<div
|
||||
v-for="ds in crosshairStats.datasets"
|
||||
:key="ds.label"
|
||||
@@ -55,6 +63,10 @@ import {
|
||||
} from 'chart.js'
|
||||
import 'chartjs-adapter-date-fns'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 300
|
||||
})
|
||||
|
||||
ChartJS.register(
|
||||
LinearScale,
|
||||
PointElement,
|
||||
@@ -87,10 +99,6 @@ interface CrosshairStats {
|
||||
totalBelowPercent: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 300
|
||||
})
|
||||
|
||||
const chartRef = ref<HTMLCanvasElement>()
|
||||
let chart: ChartJS<'scatter'> | null = null
|
||||
|
||||
@@ -252,7 +260,7 @@ const defaultOptions: ChartOptions<'scatter'> = {
|
||||
ticks: {
|
||||
color: 'rgb(107, 114, 128)',
|
||||
// 自定义刻度值:在实际值 0, 2, 5, 10, 30, 60, 120 处显示
|
||||
callback: function(this: Scale, tickValue: string | number) {
|
||||
callback(this: Scale, tickValue: string | number) {
|
||||
const displayVal = Number(tickValue)
|
||||
const realVal = toRealValue(displayVal)
|
||||
// 只在特定的显示位置显示刻度
|
||||
@@ -273,7 +281,7 @@ const defaultOptions: ChartOptions<'scatter'> = {
|
||||
text: '间隔 (分钟)',
|
||||
color: 'rgb(107, 114, 128)'
|
||||
},
|
||||
afterBuildTicks: function(scale: Scale) {
|
||||
afterBuildTicks(scale: Scale) {
|
||||
// 在特定实际值处设置刻度
|
||||
const targetTicks = [0, 2, 5, 10, 30, 60, 120]
|
||||
scale.ticks = targetTicks.map(val => ({
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<template>
|
||||
<Dialog :modelValue="modelValue" @update:modelValue="handleClose" :zIndex="80">
|
||||
<Dialog
|
||||
:model-value="modelValue"
|
||||
:z-index="80"
|
||||
@update:model-value="handleClose"
|
||||
>
|
||||
<template #header>
|
||||
<div class="border-b border-border px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<component :is="icon" class="h-5 w-5 flex-shrink-0" :class="iconColorClass" />
|
||||
<component
|
||||
:is="icon"
|
||||
class="h-5 w-5 flex-shrink-0"
|
||||
:class="iconColorClass"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">{{ title }}</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||
{{ title }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,22 +24,26 @@
|
||||
<template #default>
|
||||
<!-- 描述 -->
|
||||
<div class="space-y-3">
|
||||
<p v-for="(line, index) in descriptionLines" :key="index" :class="getLineClass(index)">
|
||||
<p
|
||||
v-for="(line, index) in descriptionLines"
|
||||
:key="index"
|
||||
:class="getLineClass(index)"
|
||||
>
|
||||
{{ line }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 自定义内容插槽 -->
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<!-- 取消按钮 -->
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="handleCancel"
|
||||
:disabled="loading"
|
||||
class="h-10 px-5"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</Button>
|
||||
@@ -37,11 +51,14 @@
|
||||
<!-- 确认按钮 -->
|
||||
<Button
|
||||
:variant="confirmVariant"
|
||||
@click="handleConfirm"
|
||||
:disabled="loading"
|
||||
class="h-10 px-5"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
<Loader2 v-if="loading" class="animate-spin h-4 w-4 mr-2" />
|
||||
<Loader2
|
||||
v-if="loading"
|
||||
class="animate-spin h-4 w-4 mr-2"
|
||||
/>
|
||||
{{ confirmText }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
@@ -3,49 +3,68 @@
|
||||
<!-- 图标 -->
|
||||
<div :class="iconContainerClasses">
|
||||
<component
|
||||
v-if="icon"
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
:class="iconClasses"
|
||||
/>
|
||||
<component
|
||||
v-else
|
||||
:is="defaultIcon"
|
||||
v-else
|
||||
:class="iconClasses"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<h3 v-if="title" :class="titleClasses">
|
||||
<h3
|
||||
v-if="title"
|
||||
:class="titleClasses"
|
||||
>
|
||||
{{ title }}
|
||||
</h3>
|
||||
|
||||
<!-- 描述 -->
|
||||
<p v-if="description" :class="descriptionClasses">
|
||||
<p
|
||||
v-if="description"
|
||||
:class="descriptionClasses"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<!-- 自定义内容插槽 -->
|
||||
<div v-if="$slots.default" class="mt-4">
|
||||
<div
|
||||
v-if="$slots.default"
|
||||
class="mt-4"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div v-if="$slots.actions || actionText" class="mt-6 flex flex-wrap items-center justify-center gap-3">
|
||||
<div
|
||||
v-if="$slots.actions || actionText"
|
||||
class="mt-6 flex flex-wrap items-center justify-center gap-3"
|
||||
>
|
||||
<slot name="actions">
|
||||
<Button
|
||||
v-if="actionText"
|
||||
@click="handleAction"
|
||||
:variant="actionVariant"
|
||||
:size="actionSize"
|
||||
@click="handleAction"
|
||||
>
|
||||
<component v-if="actionIcon" :is="actionIcon" class="mr-2 h-4 w-4" />
|
||||
<component
|
||||
:is="actionIcon"
|
||||
v-if="actionIcon"
|
||||
class="mr-2 h-4 w-4"
|
||||
/>
|
||||
{{ actionText }}
|
||||
</Button>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- 次要操作 -->
|
||||
<div v-if="$slots.secondary" class="mt-3">
|
||||
<div
|
||||
v-if="$slots.secondary"
|
||||
class="mt-3"
|
||||
>
|
||||
<slot name="secondary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
<template>
|
||||
<div :class="containerClasses">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<Skeleton v-if="variant === 'skeleton'" :class="skeletonClasses" />
|
||||
<Skeleton
|
||||
v-if="variant === 'skeleton'"
|
||||
:class="skeletonClasses"
|
||||
/>
|
||||
|
||||
<div v-else-if="variant === 'spinner'" class="relative">
|
||||
<div class="h-12 w-12 animate-spin rounded-full border-4 border-muted border-t-primary"></div>
|
||||
<div
|
||||
v-else-if="variant === 'spinner'"
|
||||
class="relative"
|
||||
>
|
||||
<div class="h-12 w-12 animate-spin rounded-full border-4 border-muted border-t-primary" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="variant === 'pulse'" class="flex gap-2">
|
||||
<div
|
||||
v-else-if="variant === 'pulse'"
|
||||
class="flex gap-2"
|
||||
>
|
||||
<div
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="h-3 w-3 animate-pulse rounded-full bg-primary"
|
||||
:style="{ animationDelay: `${i * 150}ms` }"
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="message" class="text-sm text-muted-foreground">
|
||||
<div
|
||||
v-if="message"
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="app-shell" :class="{ 'pt-24': showNotice }">
|
||||
<div
|
||||
class="app-shell"
|
||||
:class="{ 'pt-24': showNotice }"
|
||||
>
|
||||
<div
|
||||
v-if="showNotice"
|
||||
class="fixed top-6 left-0 right-0 z-50 flex justify-center px-4"
|
||||
@@ -8,8 +11,8 @@
|
||||
</div>
|
||||
|
||||
<div class="app-shell__backdrop">
|
||||
<div class="app-shell__gradient app-shell__gradient--primary"></div>
|
||||
<div class="app-shell__gradient app-shell__gradient--accent"></div>
|
||||
<div class="app-shell__gradient app-shell__gradient--primary" />
|
||||
<div class="app-shell__gradient app-shell__gradient--accent" />
|
||||
</div>
|
||||
|
||||
<div class="app-shell__body">
|
||||
@@ -21,7 +24,10 @@
|
||||
<slot name="sidebar" />
|
||||
</aside>
|
||||
|
||||
<div class="app-shell__content" :class="contentClass">
|
||||
<div
|
||||
class="app-shell__content"
|
||||
:class="contentClass"
|
||||
>
|
||||
<slot name="header" />
|
||||
<main :class="mainClass">
|
||||
<slot />
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<template>
|
||||
<Card :class="cardClasses">
|
||||
<div v-if="title || description || $slots.header" :class="headerClasses">
|
||||
<div
|
||||
v-if="title || description || $slots.header"
|
||||
:class="headerClasses"
|
||||
>
|
||||
<slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 v-if="title" class="text-lg font-medium leading-6 text-foreground">
|
||||
<h3
|
||||
v-if="title"
|
||||
class="text-lg font-medium leading-6 text-foreground"
|
||||
>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p v-if="description" class="mt-1 text-sm text-muted-foreground">
|
||||
<p
|
||||
v-if="description"
|
||||
class="mt-1 text-sm text-muted-foreground"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -22,7 +31,10 @@
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.footer" :class="footerClasses">
|
||||
<div
|
||||
v-if="$slots.footer"
|
||||
:class="footerClasses"
|
||||
>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
<!-- Logo头部 - 移动端优化 -->
|
||||
<div class="flex items-center gap-3 rounded-2xl bg-card/90 px-4 py-3 shadow-lg shadow-primary/20 ring-1 ring-border backdrop-blur">
|
||||
<div class="flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-2xl bg-background shadow-md shadow-primary/20 flex-shrink-0">
|
||||
<img src="/aether_adaptive.svg" alt="Logo" class="h-8 w-8 sm:h-10 sm:w-10" />
|
||||
<img
|
||||
src="/aether_adaptive.svg"
|
||||
alt="Logo"
|
||||
class="h-8 w-8 sm:h-10 sm:w-10"
|
||||
>
|
||||
</div>
|
||||
<!-- 文字部分 - 小屏隐藏 -->
|
||||
<div class="hidden sm:block">
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<slot name="icon">
|
||||
<div v-if="icon" class="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10">
|
||||
<component :is="icon" class="h-5 w-5 text-primary" />
|
||||
<div
|
||||
v-if="icon"
|
||||
class="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
class="h-5 w-5 text-primary"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
@@ -12,14 +18,20 @@
|
||||
<h1 class="text-2xl font-semibold text-foreground sm:text-3xl">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="description" class="mt-1 text-sm text-muted-foreground">
|
||||
<p
|
||||
v-if="description"
|
||||
class="mt-1 text-sm text-muted-foreground"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.actions" class="flex items-center gap-2">
|
||||
<div
|
||||
v-if="$slots.actions"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<template>
|
||||
<section :class="sectionClasses">
|
||||
<div v-if="title || description || $slots.header" class="mb-6">
|
||||
<div
|
||||
v-if="title || description || $slots.header"
|
||||
class="mb-6"
|
||||
>
|
||||
<slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 v-if="title" class="text-lg font-medium text-foreground">
|
||||
<h2
|
||||
v-if="title"
|
||||
class="text-lg font-medium text-foreground"
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="description" class="mt-1 text-sm text-muted-foreground">
|
||||
<p
|
||||
v-if="description"
|
||||
class="mt-1 text-sm text-muted-foreground"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
<template>
|
||||
<nav class="sidebar-nav w-full px-3">
|
||||
<div v-for="(group, index) in items" :key="index" class="space-y-1 mb-5">
|
||||
|
||||
<div
|
||||
v-for="(group, index) in items"
|
||||
:key="index"
|
||||
class="space-y-1 mb-5"
|
||||
>
|
||||
<!-- Section Header -->
|
||||
<div v-if="group.title" class="px-2.5 pb-1 flex items-center gap-2" :class="index > 0 ? 'pt-1' : ''">
|
||||
<div
|
||||
v-if="group.title"
|
||||
class="px-2.5 pb-1 flex items-center gap-2"
|
||||
:class="index > 0 ? 'pt-1' : ''"
|
||||
>
|
||||
<span class="text-[10px] font-medium text-muted-foreground/50 font-mono tabular-nums">{{ String(index + 1).padStart(2, '0') }}</span>
|
||||
<span class="text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-[0.1em]">{{ group.title }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div class="space-y-0.5">
|
||||
<template v-for="item in group.items" :key="item.href">
|
||||
<template
|
||||
v-for="item in group.items"
|
||||
:key="item.href"
|
||||
>
|
||||
<RouterLink
|
||||
:to="item.href"
|
||||
class="group relative flex items-center justify-between px-2.5 py-2 rounded-lg transition-all duration-200"
|
||||
@@ -35,7 +45,7 @@
|
||||
<div
|
||||
v-if="isItemActive(item.href)"
|
||||
class="w-1 h-1 rounded-full bg-primary"
|
||||
></div>
|
||||
/>
|
||||
</RouterLink>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -1,46 +1,100 @@
|
||||
<template>
|
||||
<div class="space-y-4 w-full overflow-visible">
|
||||
<div v-if="showHeader" class="flex items-center justify-between gap-4">
|
||||
<div
|
||||
v-if="showHeader"
|
||||
class="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
<p class="text-sm font-semibold">{{ title }}</p>
|
||||
<p v-if="subtitle" class="text-xs text-muted-foreground">{{ subtitle }}</p>
|
||||
<p class="text-sm font-semibold">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p
|
||||
v-if="subtitle"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="weekColumns.length > 0" class="flex items-center gap-1 text-[11px] text-muted-foreground flex-shrink-0">
|
||||
<div
|
||||
v-if="weekColumns.length > 0"
|
||||
class="flex items-center gap-1 text-[11px] text-muted-foreground flex-shrink-0"
|
||||
>
|
||||
<span class="flex-shrink-0">少</span>
|
||||
<div
|
||||
v-for="(level, index) in legendLevels"
|
||||
:key="index"
|
||||
class="w-3 h-3 rounded-[3px] flex-shrink-0"
|
||||
:style="getLegendStyle(level)"
|
||||
></div>
|
||||
/>
|
||||
<span class="flex-shrink-0">多</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="weekColumns.length > 0" class="flex w-full gap-3 overflow-visible">
|
||||
<div class="flex flex-col text-[10px] text-muted-foreground flex-shrink-0" :style="verticalGapStyle">
|
||||
<div
|
||||
v-if="weekColumns.length > 0"
|
||||
class="flex w-full gap-3 overflow-visible"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col text-[10px] text-muted-foreground flex-shrink-0"
|
||||
:style="verticalGapStyle"
|
||||
>
|
||||
<!-- Placeholder to align with month markers -->
|
||||
<div class="text-[10px] mb-3 invisible">M</div>
|
||||
<span :style="dayLabelStyle" class="flex items-center invisible">周日</span>
|
||||
<span :style="dayLabelStyle" class="flex items-center">一</span>
|
||||
<span :style="dayLabelStyle" class="flex items-center invisible">周二</span>
|
||||
<span :style="dayLabelStyle" class="flex items-center">三</span>
|
||||
<span :style="dayLabelStyle" class="flex items-center invisible">周四</span>
|
||||
<span :style="dayLabelStyle" class="flex items-center">五</span>
|
||||
<span :style="dayLabelStyle" class="flex items-center invisible">周六</span>
|
||||
<div class="text-[10px] mb-3 invisible">
|
||||
M
|
||||
</div>
|
||||
<span
|
||||
:style="dayLabelStyle"
|
||||
class="flex items-center invisible"
|
||||
>周日</span>
|
||||
<span
|
||||
:style="dayLabelStyle"
|
||||
class="flex items-center"
|
||||
>一</span>
|
||||
<span
|
||||
:style="dayLabelStyle"
|
||||
class="flex items-center invisible"
|
||||
>周二</span>
|
||||
<span
|
||||
:style="dayLabelStyle"
|
||||
class="flex items-center"
|
||||
>三</span>
|
||||
<span
|
||||
:style="dayLabelStyle"
|
||||
class="flex items-center invisible"
|
||||
>周四</span>
|
||||
<span
|
||||
:style="dayLabelStyle"
|
||||
class="flex items-center"
|
||||
>五</span>
|
||||
<span
|
||||
:style="dayLabelStyle"
|
||||
class="flex items-center invisible"
|
||||
>周六</span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[240px] overflow-visible">
|
||||
<div ref="heatmapWrapper" class="relative block w-full">
|
||||
<div
|
||||
ref="heatmapWrapper"
|
||||
class="relative block w-full"
|
||||
>
|
||||
<div
|
||||
v-if="tooltip.visible && tooltip.day"
|
||||
class="fixed z-10 rounded-lg border border-border/70 bg-background px-3 py-2 text-xs shadow-lg backdrop-blur pointer-events-none"
|
||||
:style="tooltipStyle"
|
||||
>
|
||||
<p class="font-medium">{{ tooltip.day.date }}</p>
|
||||
<p class="mt-0.5">{{ tooltip.day.requests }} 次请求 · {{ formatTokens(tooltip.day.total_tokens) }}</p>
|
||||
<p class="text-[11px] text-muted-foreground">成本 {{ formatCurrency(tooltip.day.total_cost) }}</p>
|
||||
<p class="font-medium">
|
||||
{{ tooltip.day.date }}
|
||||
</p>
|
||||
<p class="mt-0.5">
|
||||
{{ tooltip.day.requests }} 次请求 · {{ formatTokens(tooltip.day.total_tokens) }}
|
||||
</p>
|
||||
<p class="text-[11px] text-muted-foreground">
|
||||
成本 {{ formatCurrency(tooltip.day.total_cost) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex text-[10px] text-muted-foreground/80 mb-3" :style="horizontalGapStyle">
|
||||
<div
|
||||
class="flex text-[10px] text-muted-foreground/80 mb-3"
|
||||
:style="horizontalGapStyle"
|
||||
>
|
||||
<div
|
||||
v-for="(week, weekIndex) in weekColumns"
|
||||
:key="`month-${weekIndex}`"
|
||||
@@ -50,9 +104,21 @@
|
||||
<span v-if="monthMarkers[weekIndex]">{{ monthMarkers[weekIndex] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex" :style="horizontalGapStyle">
|
||||
<div v-for="(week, weekIndex) in weekColumns" :key="weekIndex" class="flex flex-col" :style="verticalGapStyle">
|
||||
<div v-for="(day, dayIndex) in week" :key="dayIndex" class="relative group">
|
||||
<div
|
||||
class="flex"
|
||||
:style="horizontalGapStyle"
|
||||
>
|
||||
<div
|
||||
v-for="(week, weekIndex) in weekColumns"
|
||||
:key="weekIndex"
|
||||
class="flex flex-col"
|
||||
:style="verticalGapStyle"
|
||||
>
|
||||
<div
|
||||
v-for="(day, dayIndex) in week"
|
||||
:key="dayIndex"
|
||||
class="relative group"
|
||||
>
|
||||
<div
|
||||
v-if="day"
|
||||
class="rounded-[4px] transition-all duration-200 hover:shadow-lg cursor-pointer cell-emerge"
|
||||
@@ -60,15 +126,24 @@
|
||||
:title="buildTooltip(day)"
|
||||
@mouseenter="handleHover(day, $event)"
|
||||
@mouseleave="clearHover"
|
||||
></div>
|
||||
<div v-else :style="cellSquareStyle" class="rounded-[4px] bg-transparent"></div>
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:style="cellSquareStyle"
|
||||
class="rounded-[4px] bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-xs text-muted-foreground">暂无活跃数据</p>
|
||||
<p
|
||||
v-else
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
暂无活跃数据
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user