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