mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 05:00:19 +08:00
feat(mobile): Refine mobile usage record layout
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<TableCard title="使用记录">
|
||||
<TableCard
|
||||
title="使用记录"
|
||||
class="relative"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- 时间范围筛选 -->
|
||||
<TimeRangePicker
|
||||
@@ -22,34 +25,35 @@
|
||||
class="h-8 w-full text-xs border-border/60 pl-8"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-usage-hide-unknown-toggle="mobile"
|
||||
class="h-8 w-8 shrink-0 md:hidden"
|
||||
:class="hideUnknownRecords ? 'text-primary' : ''"
|
||||
:title="hideUnknownRecords ? '显示 unknown 请求' : '隐藏 unknown 请求'"
|
||||
aria-label="隐藏 unknown 模型或提供商的请求"
|
||||
:aria-pressed="hideUnknownRecords"
|
||||
@click="$emit('update:hideUnknownRecords', !hideUnknownRecords)"
|
||||
>
|
||||
<EyeOff class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0 md:hidden"
|
||||
:class="autoRefresh ? 'text-primary' : ''"
|
||||
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新'"
|
||||
@click="$emit('update:autoRefresh', !autoRefresh)"
|
||||
>
|
||||
<RefreshCcw
|
||||
class="w-3.5 h-3.5"
|
||||
:class="autoRefresh ? 'animate-spin' : ''"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-usage-hide-unknown-toggle="mobile"
|
||||
class="absolute right-12 top-2.5 h-8 w-8 shrink-0 md:hidden"
|
||||
:class="hideUnknownRecords ? 'text-primary' : ''"
|
||||
:title="hideUnknownRecords ? '显示 unknown 请求' : '隐藏 unknown 请求'"
|
||||
aria-label="隐藏 unknown 模型或提供商的请求"
|
||||
:aria-pressed="hideUnknownRecords"
|
||||
@click="$emit('update:hideUnknownRecords', !hideUnknownRecords)"
|
||||
>
|
||||
<EyeOff class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="absolute right-4 top-2.5 h-8 w-8 shrink-0 md:hidden"
|
||||
:class="autoRefresh ? 'text-primary' : ''"
|
||||
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新'"
|
||||
@click="$emit('update:autoRefresh', !autoRefresh)"
|
||||
>
|
||||
<RefreshCcw
|
||||
class="w-3.5 h-3.5"
|
||||
:class="autoRefresh ? 'animate-spin' : ''"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<div class="order-3 grid w-full grid-cols-2 gap-2 md:hidden">
|
||||
<!-- 时间范围筛选 -->
|
||||
<TimeRangePicker
|
||||
@@ -231,15 +235,15 @@
|
||||
v-for="record in records"
|
||||
v-else
|
||||
:key="record.id"
|
||||
class="border-b border-border/40 py-2.5 px-2"
|
||||
class="border-b border-border/40 px-3 py-2.5"
|
||||
:class="isAdmin ? 'cursor-pointer active:bg-muted/30 transition-colors' : ''"
|
||||
@click="isAdmin && emit('showDetail', record.id)"
|
||||
>
|
||||
<!-- 第一行:模型 + 费用 -->
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<span class="text-sm font-medium truncate">{{ record.model }}</span>
|
||||
<div class="flex min-w-0 items-center gap-1.5">
|
||||
<span class="min-w-0 truncate text-[15px] font-semibold leading-5">{{ record.model }}</span>
|
||||
<Badge
|
||||
v-if="getReasoningEffort(record)"
|
||||
variant="outline"
|
||||
@@ -256,6 +260,55 @@
|
||||
>
|
||||
fast
|
||||
</Badge>
|
||||
<!-- 状态 Badge -->
|
||||
<Badge
|
||||
v-if="isUsageRecordFailed(record)"
|
||||
variant="destructive"
|
||||
class="whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center flex-shrink-0"
|
||||
>
|
||||
失败
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="getDisplayStatus(record) === 'pending'"
|
||||
variant="outline"
|
||||
class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center flex-shrink-0"
|
||||
>
|
||||
等待
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="getDisplayStatus(record) === 'streaming'"
|
||||
variant="outline"
|
||||
class="whitespace-nowrap animate-pulse border-primary/50 text-primary text-[10px] px-1.5 h-4 leading-4 inline-flex items-center flex-shrink-0"
|
||||
>
|
||||
传输
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="record.status === 'cancelled'"
|
||||
variant="outline"
|
||||
class="whitespace-nowrap border-amber-500/50 text-amber-600 dark:text-amber-400 text-[10px] px-1.5 h-4 leading-4 inline-flex items-center flex-shrink-0"
|
||||
>
|
||||
取消
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="getStreamModeSegments(record).hasConversion"
|
||||
:variant="streamBadgeVariant(getStreamModeSegments(record).client === '流式')"
|
||||
:class="(streamBadgeVariant(getStreamModeSegments(record).client === '流式') === 'secondary')
|
||||
? 'whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center gap-0.5 flex-shrink-0'
|
||||
: 'whitespace-nowrap border-border/60 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center gap-0.5 flex-shrink-0'"
|
||||
>
|
||||
<span>{{ getStreamModeSegments(record).client }}</span>
|
||||
<span class="opacity-60">→</span>
|
||||
<span>{{ getStreamModeSegments(record).upstream }}</span>
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
:variant="streamBadgeVariant(getUpstreamStream(record))"
|
||||
:class="(streamBadgeVariant(getUpstreamStream(record)) === 'secondary')
|
||||
? 'whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center flex-shrink-0'
|
||||
: 'whitespace-nowrap border-border/60 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center flex-shrink-0'"
|
||||
>
|
||||
{{ getStreamModeLabel(record) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<span
|
||||
v-if="getActualModel(record)"
|
||||
@@ -263,7 +316,7 @@
|
||||
>-> {{ getActualModel(record) }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-end flex-shrink-0">
|
||||
<span class="text-xs text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
|
||||
<span class="text-sm text-primary font-semibold leading-5">{{ formatCurrency(record.cost || 0) }}</span>
|
||||
<span
|
||||
v-if="showActualCost && record.actual_cost !== undefined && record.rate_multiplier && record.rate_multiplier !== 1.0"
|
||||
class="text-[10px] text-muted-foreground"
|
||||
@@ -271,79 +324,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:状态 | 时间 | API格式 | 耗时 | Tokens -->
|
||||
<div class="flex items-center justify-between text-[11px] text-muted-foreground mt-1 leading-4">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<!-- 状态 Badge -->
|
||||
<Badge
|
||||
v-if="isUsageRecordFailed(record)"
|
||||
variant="destructive"
|
||||
class="whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
|
||||
>
|
||||
失败
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="getDisplayStatus(record) === 'pending'"
|
||||
variant="outline"
|
||||
class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
|
||||
>
|
||||
等待
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="getDisplayStatus(record) === 'streaming'"
|
||||
variant="outline"
|
||||
class="whitespace-nowrap animate-pulse border-primary/50 text-primary text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
|
||||
>
|
||||
传输
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="record.status === 'cancelled'"
|
||||
variant="outline"
|
||||
class="whitespace-nowrap border-amber-500/50 text-amber-600 dark:text-amber-400 text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
|
||||
>
|
||||
取消
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="getStreamModeSegments(record).hasConversion"
|
||||
:variant="streamBadgeVariant(getStreamModeSegments(record).client === '流式')"
|
||||
:class="(streamBadgeVariant(getStreamModeSegments(record).client === '流式') === 'secondary')
|
||||
? 'whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center gap-0.5'
|
||||
: 'whitespace-nowrap border-border/60 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center gap-0.5'"
|
||||
>
|
||||
<span>{{ getStreamModeSegments(record).client }}</span>
|
||||
<span class="opacity-60">→</span>
|
||||
<span>{{ getStreamModeSegments(record).upstream }}</span>
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
:variant="streamBadgeVariant(getUpstreamStream(record))"
|
||||
:class="(streamBadgeVariant(getUpstreamStream(record)) === 'secondary')
|
||||
? 'whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center'
|
||||
: 'whitespace-nowrap border-border/60 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center'"
|
||||
>
|
||||
{{ getStreamModeLabel(record) }}
|
||||
</Badge>
|
||||
<span class="text-muted-foreground/50">|</span>
|
||||
<div class="flex flex-col leading-tight tabular-nums">
|
||||
<span class="text-[11px] text-foreground whitespace-nowrap">
|
||||
{{ formatRecordTime(record.created_at) }}
|
||||
</span>
|
||||
<span class="text-[10px] text-muted-foreground whitespace-nowrap">
|
||||
{{ formatRecordDate(record.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
<template v-if="record.api_format">
|
||||
<span class="text-muted-foreground/50">|</span>
|
||||
<span>{{ formatApiFormat(record.api_format) }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<!-- 耗时 -->
|
||||
<span
|
||||
v-if="getDisplayStatus(record) === 'pending' || getDisplayStatus(record) === 'streaming'"
|
||||
class="tabular-nums whitespace-nowrap"
|
||||
>
|
||||
<span>{{ formatRecordDurationSeconds(record.first_byte_time_ms) }}</span>
|
||||
<!-- 第二行:时间 + API格式 -->
|
||||
<div class="mt-1.5 flex min-w-0 items-center gap-1.5 text-[10px] leading-3.5 text-muted-foreground">
|
||||
<span class="shrink-0 tabular-nums text-foreground whitespace-nowrap">
|
||||
{{ formatRecordTime(record.created_at) }}
|
||||
</span>
|
||||
<span class="shrink-0 tabular-nums whitespace-nowrap">
|
||||
{{ formatRecordShortDate(record.created_at) }}
|
||||
</span>
|
||||
<template v-if="record.api_format">
|
||||
<span class="text-muted-foreground/40">·</span>
|
||||
<span class="min-w-0 truncate">{{ formatApiFormat(record.api_format) }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 第三行:性能指标 -->
|
||||
<div class="mt-1.5 flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1 text-[10px] leading-3.5 text-muted-foreground">
|
||||
<span
|
||||
class="min-w-0 truncate whitespace-nowrap tabular-nums text-foreground"
|
||||
:title="getRecordPerformanceTitle(record)"
|
||||
>
|
||||
<span class="text-muted-foreground">耗时&速度</span>
|
||||
<template v-if="getDisplayStatus(record) === 'pending' || getDisplayStatus(record) === 'streaming'">
|
||||
<span class="ml-1">{{ formatRecordDurationSeconds(record.first_byte_time_ms) }}</span>
|
||||
<span class="text-muted-foreground"> / </span>
|
||||
<ElapsedTimeText
|
||||
class="text-primary"
|
||||
@@ -351,25 +354,30 @@
|
||||
:status="getDisplayStatus(record)"
|
||||
:response-time-ms="record.response_time_ms ?? null"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-muted-foreground"> / </span>
|
||||
<span>{{ formatOutputRate(getRecordDisplayOutputRate(record)) }}</span>
|
||||
</template>
|
||||
<span
|
||||
v-else-if="record.response_time_ms != null || record.first_byte_time_ms != null"
|
||||
class="flex flex-col items-end tabular-nums leading-3 shrink-0"
|
||||
:title="getRecordPerformanceTitle(record)"
|
||||
>
|
||||
<span class="whitespace-nowrap">{{ formatRecordLatencyPair(record) }}</span>
|
||||
<span class="text-muted-foreground tabular-nums whitespace-nowrap">
|
||||
{{ formatOutputRate(getRecordDisplayOutputRate(record)) }}
|
||||
</span>
|
||||
</span>
|
||||
class="ml-1"
|
||||
>{{ formatRecordLatencyPair(record) }} / {{ formatOutputRate(getRecordDisplayOutputRate(record)) }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="tabular-nums"
|
||||
>-</span>
|
||||
<span class="text-muted-foreground/50">|</span>
|
||||
<!-- Tokens -->
|
||||
<span>{{ formatTokens(getRecordEffectiveInputTokens(record)) }}/{{ formatTokens(record.output_tokens || 0) }}</span>
|
||||
</div>
|
||||
class="ml-1"
|
||||
>- / {{ formatOutputRate(getRecordDisplayOutputRate(record)) }}</span>
|
||||
</span>
|
||||
<span class="text-muted-foreground/40">·</span>
|
||||
<span
|
||||
class="min-w-0 truncate whitespace-nowrap tabular-nums text-foreground"
|
||||
:title="hasRecordCacheTokens(record) ? getRecordCacheTokensTitle(record) : undefined"
|
||||
>
|
||||
<span class="text-muted-foreground">Tokens</span>
|
||||
<span class="ml-1">{{ formatTokens(getRecordEffectiveInputTokens(record)) }} / {{ formatTokens(record.output_tokens || 0) }}</span>
|
||||
<template v-if="hasRecordCacheTokens(record)">
|
||||
<span class="text-muted-foreground"> | </span>
|
||||
<span>{{ formatOptionalTokens(getRecordCacheReadTokens(record)) }} / {{ formatOptionalTokens(getRecordCacheCreationTokens(record)) }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1364,6 +1372,13 @@ function formatRecordDate(dateStr: string): string {
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
function formatRecordShortDate(dateStr: string): string {
|
||||
const date = parseRecordDateTime(dateStr)
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${month}-${day}`
|
||||
}
|
||||
|
||||
function formatRecordTime(dateStr: string): string {
|
||||
const date = parseRecordDateTime(dateStr)
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
@@ -1428,6 +1443,17 @@ function formatOptionalTokens(value: number | null | undefined): string {
|
||||
return hasPositiveTokens(value) ? formatTokens(value) : '-'
|
||||
}
|
||||
|
||||
function hasRecordCacheTokens(record: UsageRecord): boolean {
|
||||
return hasPositiveTokens(getRecordCacheReadTokens(record)) || hasPositiveTokens(getRecordCacheCreationTokens(record))
|
||||
}
|
||||
|
||||
function getRecordCacheTokensTitle(record: UsageRecord): string {
|
||||
return [
|
||||
`缓存读取: ${formatOptionalTokens(getRecordCacheReadTokens(record))}`,
|
||||
`缓存写入: ${formatOptionalTokens(getRecordCacheCreationTokens(record))}`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function formatRecordLatencyPair(record: UsageRecord): string {
|
||||
const firstByte = formatRecordDurationSeconds(record.first_byte_time_ms)
|
||||
const total = formatRecordDurationSeconds(record.response_time_ms)
|
||||
|
||||
Reference in New Issue
Block a user