refactor(frontend): 优化公共组件和布局组件

- 更新 Logo 相关组件 (AetherLogo, HeaderLogo, RippleLogo 等)
- 优化图表组件 (BarChart, LineChart, ScatterChart)
- 改进公共组件 (AlertDialog, EmptyState, LoadingState)
- 调整布局组件 (AppShell, SidebarNav, PageHeader 等)
- 优化 ActivityHeatmap 统计组件
This commit is contained in:
fawney19
2025-12-12 16:15:16 +08:00
parent 44e7117d4a
commit e9a6233655
21 changed files with 1130 additions and 517 deletions

View File

@@ -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)}`

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -9,15 +9,42 @@
:style="{ '--anim-delay': `${animDelay}ms` }"
>
<!-- Definitions for gradient and glow -->
<svg style="position: absolute; width: 0; height: 0; overflow: hidden;" aria-hidden="true">
<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
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" />
<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" />
@@ -27,19 +54,55 @@
</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" />
<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" />
<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" />
<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>
@@ -62,13 +125,20 @@
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
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 -->
<!-- Aether logo: single complex path with ripple effect -->
<svg
v-else-if="type === 'aether'"
:key="`aether-${animationKey}`"
@@ -78,16 +148,44 @@
:style="{ '--anim-delay': `${animDelay}ms` }"
>
<defs>
<path id="aether-path" :d="aetherPath" ref="aetherPathRef" />
<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
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" />
<filter
id="aether-glow"
x="-50%"
y="-50%"
width="200%"
height="200%"
>
<feGaussianBlur
stdDeviation="4"
result="coloredBlur"
/>
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
@@ -155,38 +253,117 @@
:style="{ '--anim-delay': `${animDelay}ms` }"
>
<defs>
<path :id="pathId" :d="pathData" />
<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
: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 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
: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 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
: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 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
: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 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
: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" />
<rect
x="-4"
y="-4"
width="32"
height="32"
fill="white"
/>
<circle
cx="12"
cy="12"
:r="geminiFillRadius"
fill="black"
/>
</mask>
</template>
</defs>
@@ -264,11 +441,30 @@
<!-- 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
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
@@ -276,10 +472,22 @@
: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)`" />
<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">
@@ -288,30 +496,66 @@
: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>
<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)`" />
<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)`" />
<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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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 => ({

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>