feat: revamp analytics dashboards and harden database migrations

Add dashboard and overview analytics, health monitoring, provider expense tracking, and announcement updates across the gateway and frontend.

Keep schema migrations free of historical backfills while preserving automatic backfill execution. Bound migration deadlines, run schema preparation before Compose replacement, and anonymize deleted dashboard users.

Include the current documentation cleanup and regression coverage.
This commit is contained in:
elky
2026-10-01 11:48:17 +08:00
parent 60b89cc840
commit 066ea87d72
327 changed files with 31728 additions and 20645 deletions
+17 -7
View File
@@ -22,21 +22,28 @@
/>
</Transition>
<div class="relative flex h-full items-end justify-center overflow-hidden text-center sm:items-center sm:p-0 pointer-events-none">
<div
class="relative flex h-full overflow-hidden pointer-events-none"
:class="isDrawer ? 'items-stretch justify-end' : 'items-end justify-center text-center sm:items-center sm:p-0'"
>
<!-- 对话框内容 -->
<Transition
:appear="isDrawer"
enter-active-class="duration-300 ease-out"
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
:enter-from-class="isDrawer ? 'translate-x-full' : 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'"
:enter-to-class="isDrawer ? 'translate-x-0' : 'opacity-100 translate-y-0 sm:scale-100'"
leave-active-class="duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
:leave-from-class="isDrawer ? 'translate-x-0' : 'opacity-100 translate-y-0 sm:scale-100'"
:leave-to-class="isDrawer ? 'translate-x-full' : 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'"
>
<div
v-if="isOpen"
class="relative flex max-h-[100dvh] w-full transform flex-col overflow-hidden rounded-t-xl border border-x-0 border-b-0 border-border bg-background text-left shadow-2xl transition-all pointer-events-auto sm:my-8 sm:w-full sm:max-h-[calc(100dvh-4rem)] sm:rounded-lg sm:border"
class="relative flex max-h-[100dvh] w-full transform flex-col overflow-hidden border-border bg-background text-left shadow-2xl transition-all pointer-events-auto"
:style="{ zIndex: contentZIndex }"
:class="maxWidthClass"
:class="[maxWidthClass, isDrawer ? 'h-full border-l' : 'rounded-t-xl border border-x-0 border-b-0 sm:my-8 sm:w-full sm:max-h-[calc(100dvh-4rem)] sm:rounded-lg sm:border']"
role="dialog"
aria-modal="true"
:aria-label="title"
@click.stop
>
<!-- Header 区域:优先使用 slot,否则使用 title prop -->
@@ -100,6 +107,7 @@ import { DIALOG_CONTEXT_KEY } from './context'
const props = defineProps<{
open?: boolean
modelValue?: boolean
placement?: 'center' | 'right'
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl'
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl'
title?: string
@@ -122,6 +130,7 @@ provide(DIALOG_CONTEXT_KEY, true)
// 获取 slots 以便在模板中使用
const slots = useSlots()
const isDrawer = computed(() => props.placement === 'right')
// 统一处理 open 状态
const isOpen = computed(() => {
@@ -170,6 +179,7 @@ const maxWidthClass = computed(() => {
const contentBodyClass = computed(() => [
'min-h-0 min-w-0 overflow-y-auto overscroll-contain',
isDrawer.value ? 'flex-1' : '',
props.noPadding ? '' : 'px-4 py-3 sm:px-6',
].filter(Boolean).join(' '))
@@ -8,12 +8,16 @@ const props = withDefaults(defineProps<{
side?: 'top' | 'right' | 'bottom' | 'left'
sideOffset?: number
alignOffset?: number
collisionPadding?: number
ariaLabel?: string
}>(), {
class: undefined,
align: 'center',
side: 'bottom',
sideOffset: 4,
alignOffset: 0,
collisionPadding: 0,
ariaLabel: undefined,
})
</script>
@@ -33,6 +37,8 @@ const props = withDefaults(defineProps<{
:side="props.side"
:side-offset="props.sideOffset"
:align-offset="props.alignOffset"
:collision-padding="props.collisionPadding"
:aria-label="props.ariaLabel"
>
<slot />
</PopoverContent>
@@ -3,13 +3,15 @@
variant="ghost"
size="icon"
class="h-8 w-8"
:disabled="loading"
:class="active ? 'text-primary' : ''"
:disabled="loading && !active"
:aria-pressed="active"
:title="title"
@click="handleClick"
>
<RefreshCcw
class="w-3.5 h-3.5"
:class="loading ? 'animate-spin' : ''"
:class="loading || active ? 'animate-spin' : ''"
/>
</Button>
</template>
@@ -20,6 +22,7 @@ import { RefreshCcw } from 'lucide-vue-next'
interface Props {
loading?: boolean
active?: boolean
title?: string
}
@@ -29,6 +32,7 @@ interface Emits {
withDefaults(defineProps<Props>(), {
loading: false,
active: undefined,
title: '刷新'
})
+12 -3
View File
@@ -8,9 +8,17 @@
<slot name="header">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<!-- 左侧:标题 -->
<h3 class="text-sm sm:text-base font-semibold shrink-0">
{{ title }}
</h3>
<div class="min-w-0">
<h3 class="text-sm sm:text-base font-semibold shrink-0">
{{ title }}
</h3>
<p
v-if="description"
class="mt-1 text-xs text-muted-foreground"
>
{{ description }}
</p>
</div>
<!-- 右侧:操作区 -->
<div
@@ -36,6 +44,7 @@ import { Card } from '@/components/ui'
interface Props {
title?: string
description?: string
}
defineProps<Props>()