mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
Show update status on admin dashboard
This commit is contained in:
@@ -122,6 +122,77 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 管理员:版本与更新状态 -->
|
||||
<Card
|
||||
v-if="isAdmin"
|
||||
class="relative mt-6 overflow-hidden p-4 sm:p-5 border-book-cloth/30"
|
||||
>
|
||||
<div class="pointer-events-none absolute -right-10 -top-10 h-32 w-32 rounded-full bg-book-cloth/20 blur-3xl" />
|
||||
<div class="relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="uppercase tracking-[0.3em] text-[10px]"
|
||||
>
|
||||
Version
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="updateStatus"
|
||||
:variant="updateStatus.has_update ? 'default' : 'secondary'"
|
||||
class="text-[10px]"
|
||||
>
|
||||
{{ updateStatusLabel }}
|
||||
</Badge>
|
||||
</div>
|
||||
<h3 class="text-sm font-medium text-foreground">
|
||||
系统版本
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
当前版本
|
||||
<span class="font-mono text-foreground">
|
||||
{{ updateStatus?.current_version || '加载中...' }}
|
||||
</span>
|
||||
<template v-if="updateStatus?.latest_version">
|
||||
,最新版本
|
||||
<span class="font-mono text-foreground">
|
||||
{{ updateStatus.latest_version }}
|
||||
</span>
|
||||
</template>
|
||||
</p>
|
||||
<p
|
||||
v-if="updateStatus?.error"
|
||||
class="mt-2 text-xs text-muted-foreground"
|
||||
>
|
||||
检查更新失败:{{ updateStatus.error }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="loadingUpdateStatus"
|
||||
@click="loadSystemUpdateStatus"
|
||||
>
|
||||
<RefreshCw
|
||||
class="mr-2 h-3.5 w-3.5"
|
||||
:class="loadingUpdateStatus ? 'animate-spin' : ''"
|
||||
/>
|
||||
重新检查
|
||||
</Button>
|
||||
<Button
|
||||
v-if="updateStatus?.has_update && updateStatus.release_url"
|
||||
size="sm"
|
||||
@click="openReleasePage"
|
||||
>
|
||||
<ExternalLink class="mr-2 h-3.5 w-3.5" />
|
||||
查看更新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 管理员:系统健康摘要 -->
|
||||
<div
|
||||
v-if="isAdmin && systemHealth"
|
||||
@@ -792,6 +863,7 @@
|
||||
import { ref, onMounted, computed, onBeforeUnmount, nextTick, watch } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { dashboardApi, type DashboardStat, type DailyStat, type ProviderSummary } from '@/api/dashboard'
|
||||
import { adminApi, type CheckUpdateResponse } from '@/api/admin'
|
||||
import { getDateRangeFromPeriod } from '@/features/usage/composables'
|
||||
import type { DateRangeParams } from '@/features/usage/types'
|
||||
import { announcementApi, type Announcement } from '@/api/announcements'
|
||||
@@ -828,10 +900,13 @@ import {
|
||||
Loader2,
|
||||
Clock,
|
||||
Database,
|
||||
Shuffle
|
||||
Shuffle,
|
||||
RefreshCw,
|
||||
ExternalLink
|
||||
} from 'lucide-vue-next'
|
||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||
import { parseDateLike } from '@/utils/date'
|
||||
import { buildDashboardUpdateErrorStatus, describeDashboardUpdateStatus } from './dashboardUpdateStatus'
|
||||
import { marked } from 'marked'
|
||||
import { sanitizeMarkdown } from '@/utils/sanitize'
|
||||
import type { ChartData, ChartOptions, ChartDataset, TooltipItem } from 'chart.js'
|
||||
@@ -970,11 +1045,17 @@ const cacheStats = ref<{
|
||||
} | null>(null)
|
||||
|
||||
const userMonthlyCost = ref<number | null>(null)
|
||||
const updateStatus = ref<CheckUpdateResponse | null>(null)
|
||||
const loadingUpdateStatus = ref(false)
|
||||
|
||||
const hasCacheData = computed(() =>
|
||||
cacheStats.value && cacheStats.value.total_cache_tokens > 0
|
||||
)
|
||||
|
||||
const updateStatusLabel = computed(() => {
|
||||
return describeDashboardUpdateStatus(updateStatus.value)
|
||||
})
|
||||
|
||||
const tokenBreakdown = ref<{
|
||||
input: number
|
||||
output: number
|
||||
@@ -1286,7 +1367,8 @@ onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadDashboardData(),
|
||||
loadAnnouncements(),
|
||||
loadDailyStats()
|
||||
loadDailyStats(),
|
||||
loadSystemUpdateStatus()
|
||||
])
|
||||
await nextTick()
|
||||
setupTimelineResizeObserver()
|
||||
@@ -1342,6 +1424,24 @@ async function loadDashboardData() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSystemUpdateStatus() {
|
||||
if (!isAdmin.value) return
|
||||
loadingUpdateStatus.value = true
|
||||
try {
|
||||
updateStatus.value = await adminApi.checkUpdate()
|
||||
} catch (error) {
|
||||
updateStatus.value = buildDashboardUpdateErrorStatus(updateStatus.value, error)
|
||||
} finally {
|
||||
loadingUpdateStatus.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openReleasePage() {
|
||||
if (updateStatus.value?.release_url) {
|
||||
window.open(updateStatus.value.release_url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDailyStats() {
|
||||
if (dailyStatsLoadPromise) {
|
||||
hasPendingDailyStatsLoad = true
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { CheckUpdateResponse } from '@/api/admin'
|
||||
import {
|
||||
buildDashboardUpdateErrorStatus,
|
||||
describeDashboardUpdateStatus,
|
||||
} from '../dashboardUpdateStatus'
|
||||
|
||||
function updateStatus(overrides: Partial<CheckUpdateResponse> = {}): CheckUpdateResponse {
|
||||
return {
|
||||
current_version: '0.7.0-rc27',
|
||||
latest_version: null,
|
||||
has_update: false,
|
||||
release_url: null,
|
||||
release_notes: null,
|
||||
published_at: null,
|
||||
error: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('dashboardUpdateStatus', () => {
|
||||
it('describes loading and latest states', () => {
|
||||
expect(describeDashboardUpdateStatus(null)).toBe('检查中')
|
||||
expect(describeDashboardUpdateStatus(updateStatus())).toBe('已是最新')
|
||||
})
|
||||
|
||||
it('prioritizes update availability over latest-version text', () => {
|
||||
expect(describeDashboardUpdateStatus(updateStatus({
|
||||
latest_version: 'v0.7.0-rc28',
|
||||
has_update: true,
|
||||
release_url: 'https://github.com/fawney19/Aether/releases/tag/v0.7.0-rc28',
|
||||
}))).toBe('有新版本')
|
||||
})
|
||||
|
||||
it('preserves the current version when building an error state', () => {
|
||||
const status = buildDashboardUpdateErrorStatus(
|
||||
updateStatus({ current_version: '0.7.0-rc28' }),
|
||||
new Error('network down')
|
||||
)
|
||||
|
||||
expect(status.current_version).toBe('0.7.0-rc28')
|
||||
expect(status.has_update).toBe(false)
|
||||
expect(status.error).toBe('network down')
|
||||
})
|
||||
})
|
||||
23
frontend/src/views/shared/dashboardUpdateStatus.ts
Normal file
23
frontend/src/views/shared/dashboardUpdateStatus.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { CheckUpdateResponse } from '@/api/admin'
|
||||
|
||||
export function describeDashboardUpdateStatus(status: CheckUpdateResponse | null): string {
|
||||
if (!status) return '检查中'
|
||||
if (status.has_update) return '有新版本'
|
||||
if (status.error) return '检查失败'
|
||||
return '已是最新'
|
||||
}
|
||||
|
||||
export function buildDashboardUpdateErrorStatus(
|
||||
previousStatus: CheckUpdateResponse | null,
|
||||
error: unknown
|
||||
): CheckUpdateResponse {
|
||||
return {
|
||||
current_version: previousStatus?.current_version || '',
|
||||
latest_version: null,
|
||||
has_update: false,
|
||||
release_url: null,
|
||||
release_notes: null,
|
||||
published_at: null,
|
||||
error: error instanceof Error ? error.message : '检查更新失败'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user