mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-09 11:12:28 +08:00
refactor(frontend): 优化功能模块组件
- 更新 api-keys 模块: StandaloneKeyFormDialog - 改进 auth 模块: LoginDialog - 优化 models 模块: AliasDialog, GlobalModelFormDialog, ModelDetailDrawer, TieredPricingEditor - 重构 providers 模块: 多个表单和对话框组件 - 更新 usage 模块: 时间线、表格和详情组件 - 调整 users 模块: UserFormDialog
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<Card class="p-4 !overflow-visible">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<p class="text-sm font-semibold">{{ title }}</p>
|
||||
<div v-if="hasData" class="flex items-center gap-1 text-[11px] text-muted-foreground flex-shrink-0">
|
||||
<p class="text-sm font-semibold">
|
||||
{{ title }}
|
||||
</p>
|
||||
<div
|
||||
v-if="hasData"
|
||||
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"
|
||||
@@ -18,7 +23,10 @@
|
||||
:data="data"
|
||||
:show-header="false"
|
||||
/>
|
||||
<div v-else class="h-full min-h-[160px] flex items-center justify-center text-sm text-muted-foreground">
|
||||
<div
|
||||
v-else
|
||||
class="h-full min-h-[160px] flex items-center justify-center text-sm text-muted-foreground"
|
||||
>
|
||||
暂无活跃数据
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
<template>
|
||||
<div class="minimal-request-timeline">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="py-4">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="py-4"
|
||||
>
|
||||
<Skeleton class="h-32 w-full" />
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<Card v-else-if="error" class="border-red-200 dark:border-red-800">
|
||||
<Card
|
||||
v-else-if="error"
|
||||
class="border-red-200 dark:border-red-800"
|
||||
>
|
||||
<div class="p-4">
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
|
||||
<p class="text-sm text-red-600 dark:text-red-400">
|
||||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Timeline Content -->
|
||||
<div v-else-if="trace && trace.candidates.length > 0" class="space-y-0">
|
||||
<div
|
||||
v-else-if="trace && trace.candidates.length > 0"
|
||||
class="space-y-0"
|
||||
>
|
||||
<Card>
|
||||
<div class="p-6">
|
||||
<!-- 概览信息 -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h4 class="text-sm font-semibold">请求链路追踪</h4>
|
||||
<h4 class="text-sm font-semibold">
|
||||
请求链路追踪
|
||||
</h4>
|
||||
<Badge :variant="getFinalStatusBadgeVariant(computedFinalStatus)">
|
||||
{{ getFinalStatusLabel(computedFinalStatus) }}
|
||||
</Badge>
|
||||
@@ -46,7 +59,9 @@
|
||||
<!-- 节点容器 -->
|
||||
<div class="node-container">
|
||||
<!-- 节点名称(在节点上方) -->
|
||||
<div class="node-label">{{ group.providerName }}</div>
|
||||
<div class="node-label">
|
||||
{{ group.providerName }}
|
||||
</div>
|
||||
|
||||
<!-- 主节点(代表首次请求) -->
|
||||
<div
|
||||
@@ -56,10 +71,13 @@
|
||||
{ 'is-first-selected': isGroupSelected(group) && selectedAttemptIndex === 0 }
|
||||
]"
|
||||
@click.stop="selectFirstAttempt(group)"
|
||||
></div>
|
||||
/>
|
||||
|
||||
<!-- 子节点(同提供商的其他尝试,不包含首次) -->
|
||||
<div v-if="group.retryCount > 0 && isGroupSelected(group)" class="sub-dots">
|
||||
<div
|
||||
v-if="group.retryCount > 0 && isGroupSelected(group)"
|
||||
class="sub-dots"
|
||||
>
|
||||
<button
|
||||
v-for="(attempt, idx) in group.allAttempts.slice(1)"
|
||||
:key="attempt.id"
|
||||
@@ -68,23 +86,32 @@
|
||||
getStatusColorClass(attempt.status),
|
||||
{ active: selectedAttemptIndex === idx + 1 }
|
||||
]"
|
||||
@click.stop="selectedAttemptIndex = idx + 1"
|
||||
:title="attempt.key_name || `Key ${idx + 2}`"
|
||||
></button>
|
||||
@click.stop="selectedAttemptIndex = idx + 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接线 -->
|
||||
<div class="node-line" v-if="groupIndex < groupedTimeline.length - 1"></div>
|
||||
<div
|
||||
v-if="groupIndex < groupedTimeline.length - 1"
|
||||
class="node-line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中详情面板 -->
|
||||
<Transition name="slide-up">
|
||||
<div v-if="selectedGroup && currentAttempt" class="detail-panel">
|
||||
<div
|
||||
v-if="selectedGroup && currentAttempt"
|
||||
class="detail-panel"
|
||||
>
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">
|
||||
<span class="title-dot" :class="getStatusColorClass(currentAttempt.status)"></span>
|
||||
<span
|
||||
class="title-dot"
|
||||
:class="getStatusColorClass(currentAttempt.status)"
|
||||
/>
|
||||
<span class="title-text">{{ selectedGroup.providerName }}</span>
|
||||
<a
|
||||
v-if="currentAttempt.provider_website"
|
||||
@@ -96,7 +123,10 @@
|
||||
>
|
||||
<ExternalLink class="w-3 h-3" />
|
||||
</a>
|
||||
<span class="status-tag" :class="getStatusColorClass(currentAttempt.status)">
|
||||
<span
|
||||
class="status-tag"
|
||||
:class="getStatusColorClass(currentAttempt.status)"
|
||||
>
|
||||
{{ currentAttempt.status_code || getStatusLabel(currentAttempt.status) }}
|
||||
</span>
|
||||
<!-- 多 Key 标识 -->
|
||||
@@ -126,31 +156,45 @@
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
|
||||
<!-- 核心信息网格 -->
|
||||
<div class="info-grid">
|
||||
<div class="info-item" v-if="currentAttempt.started_at">
|
||||
<div
|
||||
v-if="currentAttempt.started_at"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">时间范围</span>
|
||||
<span class="info-value mono time-range-value">
|
||||
{{ formatTime(currentAttempt.started_at) }}
|
||||
<span class="time-arrow-container">
|
||||
<span class="time-duration" v-if="currentAttempt.finished_at">+{{ formatDuration(currentAttempt.started_at, currentAttempt.finished_at) }}</span>
|
||||
<span
|
||||
v-if="currentAttempt.finished_at"
|
||||
class="time-duration"
|
||||
>+{{ formatDuration(currentAttempt.started_at, currentAttempt.finished_at) }}</span>
|
||||
<span class="time-arrow">→</span>
|
||||
</span>
|
||||
{{ currentAttempt.finished_at ? formatTime(currentAttempt.finished_at) : '进行中' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="currentAttempt.key_name || currentAttempt.key_id">
|
||||
<div
|
||||
v-if="currentAttempt.key_name || currentAttempt.key_id"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">密钥</span>
|
||||
<span class="info-value">
|
||||
<span class="key-name">{{ currentAttempt.key_name || '未知' }}</span>
|
||||
<template v-if="currentAttempt.key_preview">
|
||||
<Separator orientation="vertical" class="h-3 mx-2" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
class="h-3 mx-2"
|
||||
/>
|
||||
<code class="key-preview">{{ currentAttempt.key_preview }}</code>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="mergedCapabilities.length > 0">
|
||||
<div
|
||||
v-if="mergedCapabilities.length > 0"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">能力</span>
|
||||
<span class="info-value">
|
||||
<span class="capability-tags">
|
||||
@@ -166,7 +210,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 用量与费用(仅成功节点显示) -->
|
||||
<div v-if="currentAttempt.status === 'success' && usageData" class="usage-section">
|
||||
<div
|
||||
v-if="currentAttempt.status === 'success' && usageData"
|
||||
class="usage-section"
|
||||
>
|
||||
<div class="usage-grid">
|
||||
<!-- 输入 输出 -->
|
||||
<div class="usage-row">
|
||||
@@ -175,7 +222,7 @@
|
||||
<span class="usage-tokens">{{ formatNumber(usageData.tokens.input) }}</span>
|
||||
<span class="usage-cost">${{ usageData.cost.input.toFixed(6) }}</span>
|
||||
</div>
|
||||
<div class="usage-divider"></div>
|
||||
<div class="usage-divider" />
|
||||
<div class="usage-item">
|
||||
<span class="usage-label">输出</span>
|
||||
<span class="usage-tokens">{{ formatNumber(usageData.tokens.output) }}</span>
|
||||
@@ -183,13 +230,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 缓存创建 缓存读取(仅在有缓存数据时显示) -->
|
||||
<div v-if="usageData.tokens.cache_creation || usageData.tokens.cache_read" class="usage-row">
|
||||
<div
|
||||
v-if="usageData.tokens.cache_creation || usageData.tokens.cache_read"
|
||||
class="usage-row"
|
||||
>
|
||||
<div class="usage-item">
|
||||
<span class="usage-label">缓存创建</span>
|
||||
<span class="usage-tokens">{{ formatNumber(usageData.tokens.cache_creation || 0) }}</span>
|
||||
<span class="usage-cost">${{ (usageData.cost.cache_creation || 0).toFixed(6) }}</span>
|
||||
</div>
|
||||
<div class="usage-divider"></div>
|
||||
<div class="usage-divider" />
|
||||
<div class="usage-item">
|
||||
<span class="usage-label">缓存读取</span>
|
||||
<span class="usage-tokens">{{ formatNumber(usageData.tokens.cache_read || 0) }}</span>
|
||||
@@ -200,20 +250,35 @@
|
||||
</div>
|
||||
|
||||
<!-- 跳过原因 -->
|
||||
<div v-if="currentAttempt.skip_reason" class="skip-reason">
|
||||
<div
|
||||
v-if="currentAttempt.skip_reason"
|
||||
class="skip-reason"
|
||||
>
|
||||
<span class="reason-label">跳过原因</span>
|
||||
<span class="reason-value">{{ currentAttempt.skip_reason }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div v-if="currentAttempt.status === 'failed' && (currentAttempt.error_message || currentAttempt.error_type)" class="error-block">
|
||||
<div class="error-type">{{ currentAttempt.error_type || '错误' }}</div>
|
||||
<div class="error-msg">{{ currentAttempt.error_message || '未知错误' }}</div>
|
||||
<div
|
||||
v-if="currentAttempt.status === 'failed' && (currentAttempt.error_message || currentAttempt.error_type)"
|
||||
class="error-block"
|
||||
>
|
||||
<div class="error-type">
|
||||
{{ currentAttempt.error_type || '错误' }}
|
||||
</div>
|
||||
<div class="error-msg">
|
||||
{{ currentAttempt.error_message || '未知错误' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 额外数据 -->
|
||||
<details v-if="currentAttempt.extra_data && Object.keys(currentAttempt.extra_data).length > 0" class="extra-block">
|
||||
<summary class="extra-toggle">额外信息</summary>
|
||||
<details
|
||||
v-if="currentAttempt.extra_data && Object.keys(currentAttempt.extra_data).length > 0"
|
||||
class="extra-block"
|
||||
>
|
||||
<summary class="extra-toggle">
|
||||
额外信息
|
||||
</summary>
|
||||
<pre class="extra-json">{{ JSON.stringify(currentAttempt.extra_data, null, 2) }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
@@ -224,9 +289,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<Card v-else class="border-dashed">
|
||||
<Card
|
||||
v-else
|
||||
class="border-dashed"
|
||||
>
|
||||
<div class="p-8 text-center">
|
||||
<p class="text-sm text-muted-foreground">暂无追踪数据</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
暂无追踪数据
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<Card class="p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<p class="text-sm font-semibold">{{ title }}</p>
|
||||
<div v-if="displayLegendItems.length > 0" class="flex items-center gap-2 flex-wrap justify-end text-[11px]">
|
||||
<p class="text-sm font-semibold">
|
||||
{{ title }}
|
||||
</p>
|
||||
<div
|
||||
v-if="displayLegendItems.length > 0"
|
||||
class="flex items-center gap-2 flex-wrap justify-end text-[11px]"
|
||||
>
|
||||
<div
|
||||
v-for="item in displayLegendItems"
|
||||
:key="item.id"
|
||||
@@ -14,18 +19,35 @@
|
||||
/>
|
||||
<span class="text-muted-foreground">{{ item.name }}</span>
|
||||
</div>
|
||||
<span v-if="hiddenLegendCount > 0" class="text-muted-foreground">
|
||||
<span
|
||||
v-if="hiddenLegendCount > 0"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
+{{ hiddenLegendCount }} 更多
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="h-[160px] flex items-center justify-center">
|
||||
<div class="text-sm text-muted-foreground">Loading...</div>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="h-[160px] flex items-center justify-center"
|
||||
>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="hasData" class="h-[160px]">
|
||||
<ScatterChart :data="chartData" :options="chartOptions" />
|
||||
<div
|
||||
v-else-if="hasData"
|
||||
class="h-[160px]"
|
||||
>
|
||||
<ScatterChart
|
||||
:data="chartData"
|
||||
:options="chartOptions"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="h-[160px] flex items-center justify-center text-sm text-muted-foreground">
|
||||
<div
|
||||
v-else
|
||||
class="h-[160px] flex items-center justify-center text-sm text-muted-foreground"
|
||||
>
|
||||
暂无请求间隔数据
|
||||
</div>
|
||||
</Card>
|
||||
@@ -147,7 +169,7 @@ function formatModelName(model: string): string {
|
||||
}
|
||||
}
|
||||
// 其他模型保持原样但截断
|
||||
return model.length > 20 ? model.slice(0, 17) + '...' : model
|
||||
return model.length > 20 ? `${model.slice(0, 17) }...` : model
|
||||
}
|
||||
|
||||
// 构建图表数据
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
@click.self="handleClose"
|
||||
>
|
||||
<!-- 背景遮罩 -->
|
||||
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="handleClose"></div>
|
||||
<div
|
||||
class="absolute inset-0 bg-black/30 backdrop-blur-sm"
|
||||
@click="handleClose"
|
||||
/>
|
||||
|
||||
<!-- 抽屉内容 -->
|
||||
<Card class="relative h-full w-[800px] max-w-[90vw] rounded-none shadow-2xl flex flex-col">
|
||||
@@ -17,19 +20,46 @@
|
||||
<!-- 第一行:标题、模型、状态、操作按钮 -->
|
||||
<div class="flex items-center justify-between gap-4 mb-3">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h3 class="text-lg font-semibold">请求详情</h3>
|
||||
<h3 class="text-lg font-semibold">
|
||||
请求详情
|
||||
</h3>
|
||||
<div class="flex items-center gap-1 text-sm font-mono text-muted-foreground bg-muted px-2 py-0.5 rounded">
|
||||
<span>{{ detail?.model || '-' }}</span>
|
||||
<template v-if="detail?.target_model">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 flex-shrink-0">
|
||||
<path fill-rule="evenodd" d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z" clip-rule="evenodd" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-3 h-3 flex-shrink-0"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ detail.target_model }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<Badge v-if="detail?.status_code === 200" variant="success">{{ detail.status_code }}</Badge>
|
||||
<Badge v-else-if="detail" variant="destructive">{{ detail.status_code }}</Badge>
|
||||
<Badge variant="outline" class="text-xs" v-if="detail">{{ detail.is_stream ? '流式' : '标准' }}</Badge>
|
||||
<Badge
|
||||
v-if="detail?.status_code === 200"
|
||||
variant="success"
|
||||
>
|
||||
{{ detail.status_code }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="detail"
|
||||
variant="destructive"
|
||||
>
|
||||
{{ detail.status_code }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="detail"
|
||||
variant="outline"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ detail.is_stream ? '流式' : '标准' }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
@@ -37,18 +67,30 @@
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:disabled="loading"
|
||||
@click="refreshDetail"
|
||||
title="刷新"
|
||||
@click="refreshDetail"
|
||||
>
|
||||
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': loading }" />
|
||||
<RefreshCw
|
||||
class="w-4 h-4"
|
||||
:class="{ 'animate-spin': loading }"
|
||||
/>
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="handleClose" title="关闭">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="关闭"
|
||||
@click="handleClose"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 第二行:关键元信息 -->
|
||||
<div v-if="detail" class="flex items-center flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||
<div
|
||||
v-if="detail"
|
||||
class="flex items-center flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground"
|
||||
>
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="font-medium text-foreground">ID:</span>
|
||||
<span class="font-mono">{{ detail.request_id || detail.id }}</span>
|
||||
@@ -67,21 +109,32 @@
|
||||
<!-- 可滚动内容区域 -->
|
||||
<div class="flex-1 min-h-0 overflow-y-auto px-6 py-4 scrollbar-stable">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="py-8 space-y-4">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="py-8 space-y-4"
|
||||
>
|
||||
<Skeleton class="h-8 w-full" />
|
||||
<Skeleton class="h-32 w-full" />
|
||||
<Skeleton class="h-64 w-full" />
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<Card v-else-if="error" class="border-red-200 dark:border-red-800">
|
||||
<Card
|
||||
v-else-if="error"
|
||||
class="border-red-200 dark:border-red-800"
|
||||
>
|
||||
<div class="p-4">
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
|
||||
<p class="text-sm text-red-600 dark:text-red-400">
|
||||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Detail Content -->
|
||||
<div v-else-if="detail" class="space-y-4">
|
||||
<div
|
||||
v-else-if="detail"
|
||||
class="space-y-4"
|
||||
>
|
||||
<!-- 费用与性能概览 -->
|
||||
<Card>
|
||||
<div class="p-4">
|
||||
@@ -93,7 +146,10 @@
|
||||
${{ ((typeof detail.cost === 'object' ? detail.cost?.total : detail.cost) || detail.total_cost || 0).toFixed(6) }}
|
||||
</span>
|
||||
</div>
|
||||
<Separator orientation="vertical" class="h-6 mx-6" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
class="h-6 mx-6"
|
||||
/>
|
||||
<div class="flex items-center">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">响应时间</span>
|
||||
<span class="text-lg font-bold">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).value : 'N/A' }}</span>
|
||||
@@ -111,7 +167,11 @@
|
||||
<span class="text-foreground">|</span>
|
||||
<span>总输入上下文: <span class="font-mono font-medium text-foreground">{{ formatNumber(totalInputContext) }}</span></span>
|
||||
<span class="text-muted-foreground/60">(输入 {{ formatNumber(detail.tokens?.input || detail.input_tokens || 0) }} + 缓存创建 {{ formatNumber(detail.cache_creation_input_tokens || 0) }} + 缓存读取 {{ formatNumber(detail.cache_read_input_tokens || 0) }})</span>
|
||||
<Badge v-if="displayTiers.length > 1" variant="outline" class="text-[10px] px-1.5 py-0 h-4">
|
||||
<Badge
|
||||
v-if="displayTiers.length > 1"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1.5 py-0 h-4"
|
||||
>
|
||||
命中第 {{ currentTierIndex + 1 }} 阶
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -129,13 +189,20 @@
|
||||
<!-- 阶梯标题行 -->
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium" :class="index === currentTierIndex ? 'text-primary' : 'text-muted-foreground'">
|
||||
<span
|
||||
class="font-medium"
|
||||
:class="index === currentTierIndex ? 'text-primary' : 'text-muted-foreground'"
|
||||
>
|
||||
第 {{ index + 1 }} 阶
|
||||
</span>
|
||||
<span class="text-muted-foreground">
|
||||
{{ getTierRangeText(tier, index, displayTiers) }}
|
||||
</span>
|
||||
<Badge v-if="index === currentTierIndex" variant="default" class="text-[10px] px-1.5 py-0 h-4">
|
||||
<Badge
|
||||
v-if="index === currentTierIndex"
|
||||
variant="default"
|
||||
class="text-[10px] px-1.5 py-0 h-4"
|
||||
>
|
||||
当前
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -161,7 +228,10 @@
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.tokens?.input || detail.input_tokens || 0 }}</span>
|
||||
<span class="text-xs font-mono">${{ (detail.cost?.input || detail.input_cost || 0).toFixed(6) }}</span>
|
||||
</div>
|
||||
<Separator orientation="vertical" class="h-4 mx-4" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
class="h-4 mx-4"
|
||||
/>
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">输出</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.tokens?.output || detail.output_tokens || 0 }}</span>
|
||||
@@ -175,7 +245,10 @@
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.cache_creation_input_tokens || 0 }}</span>
|
||||
<span class="text-xs font-mono">${{ (detail.cache_creation_cost || 0).toFixed(6) }}</span>
|
||||
</div>
|
||||
<Separator orientation="vertical" class="h-4 mx-4" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
class="h-4 mx-4"
|
||||
/>
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">缓存读取</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.cache_read_input_tokens || 0 }}</span>
|
||||
@@ -183,13 +256,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 按次计费 -->
|
||||
<div v-if="detail.request_cost" class="flex items-center">
|
||||
<div
|
||||
v-if="detail.request_cost"
|
||||
class="flex items-center"
|
||||
>
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">按次计费</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center"></span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center" />
|
||||
<span class="text-xs font-mono">${{ detail.request_cost.toFixed(6) }}</span>
|
||||
</div>
|
||||
<Separator orientation="vertical" class="h-4 mx-4 invisible" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
class="h-4 mx-4 invisible"
|
||||
/>
|
||||
<div class="flex items-center flex-1 invisible">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">占位</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">0</span>
|
||||
@@ -211,11 +290,18 @@
|
||||
</div>
|
||||
|
||||
<!-- 错误信息卡片 -->
|
||||
<Card v-if="detail.error_message" class="border-red-200 dark:border-red-800">
|
||||
<Card
|
||||
v-if="detail.error_message"
|
||||
class="border-red-200 dark:border-red-800"
|
||||
>
|
||||
<div class="p-4">
|
||||
<h4 class="text-sm font-semibold text-red-600 dark:text-red-400 mb-2">错误信息</h4>
|
||||
<h4 class="text-sm font-semibold text-red-600 dark:text-red-400 mb-2">
|
||||
错误信息
|
||||
</h4>
|
||||
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">
|
||||
<p class="text-sm text-red-800 dark:text-red-300">{{ detail.error_message }}</p>
|
||||
<p class="text-sm text-red-800 dark:text-red-300">
|
||||
{{ detail.error_message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -223,7 +309,10 @@
|
||||
<!-- Tabs 区域 -->
|
||||
<Card>
|
||||
<div class="p-4">
|
||||
<Tabs v-model="activeTab" :default-value="activeTab">
|
||||
<Tabs
|
||||
v-model="activeTab"
|
||||
:default-value="activeTab"
|
||||
>
|
||||
<!-- Tab + 图标工具栏同行 -->
|
||||
<div class="flex items-center justify-between border-b pb-2 mb-3">
|
||||
<!-- 左侧 Tab -->
|
||||
@@ -231,11 +320,11 @@
|
||||
<button
|
||||
v-for="tab in visibleTabs"
|
||||
:key="tab.name"
|
||||
@click="activeTab = tab.name"
|
||||
class="px-3 py-1.5 text-sm transition-colors border-b-2 -mb-[9px]"
|
||||
:class="activeTab === tab.name
|
||||
? 'border-primary text-foreground font-medium'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'"
|
||||
@click="activeTab = tab.name"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
@@ -245,56 +334,71 @@
|
||||
<!-- 请求头专用:对比/客户端/提供商 切换组 -->
|
||||
<template v-if="activeTab === 'request-headers' && hasProviderHeaders">
|
||||
<button
|
||||
:title="'对比'"
|
||||
@click="viewMode = 'compare'"
|
||||
title="对比"
|
||||
class="p-1.5 rounded transition-colors"
|
||||
:class="viewMode === 'compare' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'"
|
||||
@click="viewMode = 'compare'"
|
||||
>
|
||||
<Columns2 class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
:title="'客户端'"
|
||||
@click="viewMode = 'formatted'; dataSource = 'client'"
|
||||
title="客户端"
|
||||
class="p-1.5 rounded transition-colors"
|
||||
:class="viewMode === 'formatted' && dataSource === 'client' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'"
|
||||
@click="viewMode = 'formatted'; dataSource = 'client'"
|
||||
>
|
||||
<Monitor class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
:title="'提供商'"
|
||||
@click="viewMode = 'formatted'; dataSource = 'provider'"
|
||||
title="提供商"
|
||||
class="p-1.5 rounded transition-colors"
|
||||
:class="viewMode === 'formatted' && dataSource === 'provider' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'"
|
||||
@click="viewMode = 'formatted'; dataSource = 'provider'"
|
||||
>
|
||||
<Server class="w-4 h-4" />
|
||||
</button>
|
||||
<Separator orientation="vertical" class="h-4 mx-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
class="h-4 mx-1"
|
||||
/>
|
||||
</template>
|
||||
<!-- 展开/收缩 -->
|
||||
<button
|
||||
:title="currentExpandDepth === 0 ? '展开全部' : '收缩全部'"
|
||||
@click="currentExpandDepth === 0 ? expandAll() : collapseAll()"
|
||||
class="p-1.5 rounded transition-colors"
|
||||
:class="viewMode === 'compare'
|
||||
? 'text-muted-foreground/40 cursor-not-allowed'
|
||||
: 'text-muted-foreground hover:bg-muted'"
|
||||
:disabled="viewMode === 'compare'"
|
||||
@click="currentExpandDepth === 0 ? expandAll() : collapseAll()"
|
||||
>
|
||||
<Maximize2 v-if="currentExpandDepth === 0" class="w-4 h-4" />
|
||||
<Minimize2 v-else class="w-4 h-4" />
|
||||
<Maximize2
|
||||
v-if="currentExpandDepth === 0"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<Minimize2
|
||||
v-else
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</button>
|
||||
<!-- 复制 -->
|
||||
<button
|
||||
:title="copiedStates[activeTab] ? '已复制' : '复制'"
|
||||
@click="copyJsonToClipboard(activeTab)"
|
||||
class="p-1.5 rounded transition-colors"
|
||||
:class="viewMode === 'compare'
|
||||
? 'text-muted-foreground/40 cursor-not-allowed'
|
||||
: 'text-muted-foreground hover:bg-muted'"
|
||||
:disabled="viewMode === 'compare'"
|
||||
@click="copyJsonToClipboard(activeTab)"
|
||||
>
|
||||
<Check v-if="copiedStates[activeTab]" class="w-4 h-4 text-green-500" />
|
||||
<Copy v-else class="w-4 h-4" />
|
||||
<Check
|
||||
v-if="copiedStates[activeTab]"
|
||||
class="w-4 h-4 text-green-500"
|
||||
/>
|
||||
<Copy
|
||||
v-else
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -613,9 +717,9 @@ function formatApiFormat(format: string | null | undefined): string {
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
if (num >= 1_000_000) {
|
||||
return (num / 1_000_000).toFixed(1) + 'M'
|
||||
return `${(num / 1_000_000).toFixed(1) }M`
|
||||
} else if (num >= 1_000) {
|
||||
return (num / 1_000).toFixed(1) + 'K'
|
||||
return `${(num / 1_000).toFixed(1) }K`
|
||||
}
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!data || (typeof data === 'object' && Object.keys(data).length === 0)" class="text-sm text-muted-foreground">
|
||||
<div
|
||||
v-if="!data || (typeof data === 'object' && Object.keys(data).length === 0)"
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
{{ emptyMessage }}
|
||||
</div>
|
||||
<!-- 纯字符串数据(非 JSON 对象) -->
|
||||
<Card v-else-if="typeof data === 'string'" class="bg-muted/30 overflow-hidden">
|
||||
<Card
|
||||
v-else-if="typeof data === 'string'"
|
||||
class="bg-muted/30 overflow-hidden"
|
||||
>
|
||||
<div class="p-4 overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ data }}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
<!-- 非 JSON 响应(如 HTML 错误页面) -->
|
||||
<Card v-else-if="data.raw_response && data.metadata?.parse_error" class="bg-muted/30 overflow-hidden">
|
||||
<Card
|
||||
v-else-if="data.raw_response && data.metadata?.parse_error"
|
||||
class="bg-muted/30 overflow-hidden"
|
||||
>
|
||||
<div class="p-3 bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-amber-600 dark:text-amber-400 text-sm font-medium">Warning: 响应解析失败</span>
|
||||
@@ -21,12 +30,24 @@
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap text-muted-foreground">{{ data.raw_response }}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
<Card v-else class="bg-muted/30 overflow-hidden">
|
||||
<Card
|
||||
v-else
|
||||
class="bg-muted/30 overflow-hidden"
|
||||
>
|
||||
<!-- JSON 查看器 -->
|
||||
<div class="json-viewer" :class="{ 'theme-dark': isDark }">
|
||||
<div
|
||||
class="json-viewer"
|
||||
:class="{ 'theme-dark': isDark }"
|
||||
>
|
||||
<div class="json-lines">
|
||||
<template v-for="line in visibleLines" :key="line.displayId">
|
||||
<div class="json-line" :class="{ 'has-fold': line.canFold }">
|
||||
<template
|
||||
v-for="line in visibleLines"
|
||||
:key="line.displayId"
|
||||
>
|
||||
<div
|
||||
class="json-line"
|
||||
:class="{ 'has-fold': line.canFold }"
|
||||
>
|
||||
<!-- 行号区域(包含折叠按钮) -->
|
||||
<div class="line-number-area">
|
||||
<span
|
||||
@@ -34,22 +55,31 @@
|
||||
class="fold-button"
|
||||
@click="toggleFold(line.blockId)"
|
||||
>
|
||||
<ChevronRight v-if="collapsedBlocks.has(line.blockId)" class="fold-icon" />
|
||||
<ChevronDown v-else class="fold-icon" />
|
||||
<ChevronRight
|
||||
v-if="collapsedBlocks.has(line.blockId)"
|
||||
class="fold-icon"
|
||||
/>
|
||||
<ChevronDown
|
||||
v-else
|
||||
class="fold-icon"
|
||||
/>
|
||||
</span>
|
||||
<span class="line-number">{{ line.displayLineNumber }}</span>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="line-content-area">
|
||||
<!-- 缩进 -->
|
||||
<span class="indent" :style="{ width: `${line.indent * 16}px` }"></span>
|
||||
<span
|
||||
class="indent"
|
||||
:style="{ width: `${line.indent * 16}px` }"
|
||||
/>
|
||||
<!-- 内容 -->
|
||||
<span
|
||||
class="line-content"
|
||||
:class="{ 'clickable-collapsed': line.canFold && collapsedBlocks.has(line.blockId) }"
|
||||
@click="line.canFold && collapsedBlocks.has(line.blockId) && toggleFold(line.blockId)"
|
||||
v-html="getDisplayHtml(line)"
|
||||
></span>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
<div>
|
||||
<!-- 对比模式 - 并排 Diff -->
|
||||
<div v-show="viewMode === 'compare'">
|
||||
<div v-if="!detail.request_headers && !detail.provider_request_headers" class="text-sm text-muted-foreground">
|
||||
<div
|
||||
v-if="!detail.request_headers && !detail.provider_request_headers"
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
无请求头信息
|
||||
</div>
|
||||
<Card v-else class="bg-muted/30 overflow-hidden">
|
||||
<Card
|
||||
v-else
|
||||
class="bg-muted/30 overflow-hidden"
|
||||
>
|
||||
<!-- Diff 头部 -->
|
||||
<div class="flex border-b bg-muted/50">
|
||||
<div class="flex-1 px-3 py-2 text-xs text-muted-foreground border-r flex items-center justify-between">
|
||||
@@ -23,25 +29,40 @@
|
||||
<div class="flex font-mono text-xs">
|
||||
<!-- 左侧:客户端 -->
|
||||
<div class="flex-1 border-r">
|
||||
<template v-for="entry in sortedEntries" :key="'left-' + entry.key">
|
||||
<template
|
||||
v-for="entry in sortedEntries"
|
||||
:key="'left-' + entry.key"
|
||||
>
|
||||
<!-- 删除的行 -->
|
||||
<div v-if="entry.status === 'removed'" class="flex items-start bg-destructive/10 px-3 py-0.5">
|
||||
<div
|
||||
v-if="entry.status === 'removed'"
|
||||
class="flex items-start bg-destructive/10 px-3 py-0.5"
|
||||
>
|
||||
<span class="text-destructive">
|
||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||
</span>
|
||||
</div>
|
||||
<!-- 修改的行 - 旧值 -->
|
||||
<div v-else-if="entry.status === 'modified'" class="flex items-start bg-amber-500/10 px-3 py-0.5">
|
||||
<div
|
||||
v-else-if="entry.status === 'modified'"
|
||||
class="flex items-start bg-amber-500/10 px-3 py-0.5"
|
||||
>
|
||||
<span class="text-amber-600 dark:text-amber-400">
|
||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||
</span>
|
||||
</div>
|
||||
<!-- 新增的行 - 左侧空白占位 -->
|
||||
<div v-else-if="entry.status === 'added'" class="flex items-start bg-muted/30 px-3 py-0.5">
|
||||
<div
|
||||
v-else-if="entry.status === 'added'"
|
||||
class="flex items-start bg-muted/30 px-3 py-0.5"
|
||||
>
|
||||
<span class="text-muted-foreground/30 italic">(无)</span>
|
||||
</div>
|
||||
<!-- 未变化的行 -->
|
||||
<div v-else class="flex items-start px-3 py-0.5 hover:bg-muted/50">
|
||||
<div
|
||||
v-else
|
||||
class="flex items-start px-3 py-0.5 hover:bg-muted/50"
|
||||
>
|
||||
<span class="text-muted-foreground">
|
||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||
</span>
|
||||
@@ -50,27 +71,42 @@
|
||||
</div>
|
||||
<!-- 右侧:提供商 -->
|
||||
<div class="flex-1">
|
||||
<template v-for="entry in sortedEntries" :key="'right-' + entry.key">
|
||||
<template
|
||||
v-for="entry in sortedEntries"
|
||||
:key="'right-' + entry.key"
|
||||
>
|
||||
<!-- 删除的行 - 右侧空白占位 -->
|
||||
<div v-if="entry.status === 'removed'" class="flex items-start bg-muted/30 px-3 py-0.5">
|
||||
<div
|
||||
v-if="entry.status === 'removed'"
|
||||
class="flex items-start bg-muted/30 px-3 py-0.5"
|
||||
>
|
||||
<span class="text-muted-foreground/50 line-through">
|
||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||
</span>
|
||||
</div>
|
||||
<!-- 修改的行 - 新值 -->
|
||||
<div v-else-if="entry.status === 'modified'" class="flex items-start bg-amber-500/10 px-3 py-0.5">
|
||||
<div
|
||||
v-else-if="entry.status === 'modified'"
|
||||
class="flex items-start bg-amber-500/10 px-3 py-0.5"
|
||||
>
|
||||
<span class="text-amber-600 dark:text-amber-400">
|
||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||
</span>
|
||||
</div>
|
||||
<!-- 新增的行 -->
|
||||
<div v-else-if="entry.status === 'added'" class="flex items-start bg-green-500/10 px-3 py-0.5">
|
||||
<div
|
||||
v-else-if="entry.status === 'added'"
|
||||
class="flex items-start bg-green-500/10 px-3 py-0.5"
|
||||
>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||
</span>
|
||||
</div>
|
||||
<!-- 未变化的行 -->
|
||||
<div v-else class="flex items-start px-3 py-0.5 hover:bg-muted/50">
|
||||
<div
|
||||
v-else
|
||||
class="flex items-start px-3 py-0.5 hover:bg-muted/50"
|
||||
>
|
||||
<span class="text-muted-foreground">
|
||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||
</span>
|
||||
@@ -95,10 +131,16 @@
|
||||
|
||||
<!-- 原始模式 -->
|
||||
<div v-show="viewMode === 'raw'">
|
||||
<div v-if="!currentHeaderData || Object.keys(currentHeaderData).length === 0" class="text-sm text-muted-foreground">
|
||||
<div
|
||||
v-if="!currentHeaderData || Object.keys(currentHeaderData).length === 0"
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
无请求头信息
|
||||
</div>
|
||||
<Card v-else class="bg-muted/30">
|
||||
<Card
|
||||
v-else
|
||||
class="bg-muted/30"
|
||||
>
|
||||
<div class="p-4 overflow-x-auto">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ JSON.stringify(currentHeaderData, null, 2) }}</pre>
|
||||
</div>
|
||||
|
||||
@@ -1,41 +1,66 @@
|
||||
<template>
|
||||
<Card class="overflow-hidden">
|
||||
<div class="px-3 py-2 border-b">
|
||||
<h3 class="text-sm font-medium">按API格式分析</h3>
|
||||
<h3 class="text-sm font-medium">
|
||||
按API格式分析
|
||||
</h3>
|
||||
</div>
|
||||
<Table class="text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="h-8 px-2">API格式</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">请求数</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">Tokens</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">费用</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">平均响应</TableHead>
|
||||
<TableHead class="h-8 px-2">
|
||||
API格式
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
请求数
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
Tokens
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
费用
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
平均响应
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="data.length === 0">
|
||||
<TableCell :colspan="5" class="text-center py-6 text-muted-foreground px-2">
|
||||
<TableCell
|
||||
:colspan="5"
|
||||
class="text-center py-6 text-muted-foreground px-2"
|
||||
>
|
||||
暂无API格式统计数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="item in data" :key="item.api_format">
|
||||
<TableRow
|
||||
v-for="item in data"
|
||||
:key="item.api_format"
|
||||
>
|
||||
<TableCell class="font-medium py-2 px-2">
|
||||
{{ formatApiFormat(item.api_format) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">{{ item.request_count }}</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
{{ item.request_count }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
<span>{{ formatTokens(item.total_tokens) }}</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||
<span class="text-primary font-medium">{{ formatCurrency(item.total_cost) }}</span>
|
||||
<span v-if="isAdmin && item.actual_cost !== undefined" class="text-muted-foreground text-[10px]">
|
||||
<span
|
||||
v-if="isAdmin && item.actual_cost !== undefined"
|
||||
class="text-muted-foreground text-[10px]"
|
||||
>
|
||||
{{ formatCurrency(item.actual_cost) }}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground py-2 px-2">{{ item.avgResponseTime }}</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground py-2 px-2">
|
||||
{{ item.avgResponseTime }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -54,6 +79,11 @@ import TableCell from '@/components/ui/table-cell.vue'
|
||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||
import type { ApiFormatStatsItem } from '../types'
|
||||
|
||||
defineProps<{
|
||||
data: ApiFormatStatsItem[]
|
||||
isAdmin: boolean
|
||||
}>()
|
||||
|
||||
// 格式化 API 格式显示名称
|
||||
function formatApiFormat(format: string): string {
|
||||
const formatMap: Record<string, string> = {
|
||||
@@ -67,8 +97,4 @@ function formatApiFormat(format: string): string {
|
||||
return formatMap[format.toUpperCase()] || format
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
data: ApiFormatStatsItem[]
|
||||
isAdmin: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -1,41 +1,66 @@
|
||||
<template>
|
||||
<Card class="overflow-hidden">
|
||||
<div class="px-3 py-2 border-b">
|
||||
<h3 class="text-sm font-medium">按模型分析</h3>
|
||||
<h3 class="text-sm font-medium">
|
||||
按模型分析
|
||||
</h3>
|
||||
</div>
|
||||
<Table class="text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="h-8 px-2">模型</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">请求数</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">Tokens</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">费用</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">效率</TableHead>
|
||||
<TableHead class="h-8 px-2">
|
||||
模型
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
请求数
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
Tokens
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
费用
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
效率
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="data.length === 0">
|
||||
<TableCell :colspan="5" class="text-center py-6 text-muted-foreground px-2">
|
||||
<TableCell
|
||||
:colspan="5"
|
||||
class="text-center py-6 text-muted-foreground px-2"
|
||||
>
|
||||
暂无模型统计数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="model in data" :key="model.model">
|
||||
<TableRow
|
||||
v-for="model in data"
|
||||
:key="model.model"
|
||||
>
|
||||
<TableCell class="font-medium py-2 px-2">
|
||||
{{ model.model.replace('claude-', '') }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">{{ model.request_count }}</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
{{ model.request_count }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
<span>{{ formatTokens(model.total_tokens) }}</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||
<span class="text-primary font-medium">{{ formatCurrency(model.total_cost) }}</span>
|
||||
<span v-if="isAdmin && model.actual_cost !== undefined" class="text-muted-foreground text-[10px]">
|
||||
<span
|
||||
v-if="isAdmin && model.actual_cost !== undefined"
|
||||
class="text-muted-foreground text-[10px]"
|
||||
>
|
||||
{{ formatCurrency(model.actual_cost) }}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground py-2 px-2">{{ model.costPerToken }}</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground py-2 px-2">
|
||||
{{ model.costPerToken }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -1,35 +1,62 @@
|
||||
<template>
|
||||
<Card class="overflow-hidden">
|
||||
<div class="px-3 py-2 border-b">
|
||||
<h3 class="text-sm font-medium">按提供商分析</h3>
|
||||
<h3 class="text-sm font-medium">
|
||||
按提供商分析
|
||||
</h3>
|
||||
</div>
|
||||
<Table class="text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="h-8 px-2">提供商</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">请求数</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">Tokens</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">费用</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">成功率</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">平均响应</TableHead>
|
||||
<TableHead class="h-8 px-2">
|
||||
提供商
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
请求数
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
Tokens
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
费用
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
成功率
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
平均响应
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="data.length === 0">
|
||||
<TableCell :colspan="6" class="text-center py-6 text-muted-foreground px-2">
|
||||
<TableCell
|
||||
:colspan="6"
|
||||
class="text-center py-6 text-muted-foreground px-2"
|
||||
>
|
||||
暂无提供商统计数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="provider in data" :key="provider.provider">
|
||||
<TableCell class="font-medium py-2 px-2">{{ provider.provider }}</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">{{ provider.requests }}</TableCell>
|
||||
<TableRow
|
||||
v-for="provider in data"
|
||||
:key="provider.provider"
|
||||
>
|
||||
<TableCell class="font-medium py-2 px-2">
|
||||
{{ provider.provider }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
{{ provider.requests }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
<span>{{ formatTokens(provider.totalTokens) }}</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||
<span class="text-primary font-medium">{{ formatCurrency(provider.totalCost) }}</span>
|
||||
<span v-if="isAdmin && provider.actualCost !== undefined" class="text-muted-foreground text-[10px]">
|
||||
<span
|
||||
v-if="isAdmin && provider.actualCost !== undefined"
|
||||
class="text-muted-foreground text-[10px]"
|
||||
>
|
||||
{{ formatCurrency(provider.actualCost) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -37,7 +64,9 @@
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
<span :class="getSuccessRateClass(provider.successRate)">{{ provider.successRate }}%</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground py-2 px-2">{{ provider.avgResponseTime }}</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground py-2 px-2">
|
||||
{{ provider.avgResponseTime }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -56,14 +85,15 @@ import TableCell from '@/components/ui/table-cell.vue'
|
||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||
import type { ProviderStatsItem } from '../types'
|
||||
|
||||
defineProps<{
|
||||
data: ProviderStatsItem[]
|
||||
isAdmin: boolean
|
||||
}>()
|
||||
|
||||
// 成功率样式 - 简化为两种状态
|
||||
function getSuccessRateClass(rate: number): string {
|
||||
if (rate < 90) return 'text-destructive'
|
||||
return '' // 默认颜色
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
data: ProviderStatsItem[]
|
||||
isAdmin: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -3,19 +3,29 @@
|
||||
<template #actions>
|
||||
<!-- 时间段筛选 -->
|
||||
<Select
|
||||
:model-value="selectedPeriod"
|
||||
v-model:open="periodSelectOpen"
|
||||
:model-value="selectedPeriod"
|
||||
@update:model-value="$emit('update:selectedPeriod', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-32 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="选择时间段" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="today">今天</SelectItem>
|
||||
<SelectItem value="yesterday">昨天</SelectItem>
|
||||
<SelectItem value="last7days">最近7天</SelectItem>
|
||||
<SelectItem value="last30days">最近30天</SelectItem>
|
||||
<SelectItem value="last90days">最近90天</SelectItem>
|
||||
<SelectItem value="today">
|
||||
今天
|
||||
</SelectItem>
|
||||
<SelectItem value="yesterday">
|
||||
昨天
|
||||
</SelectItem>
|
||||
<SelectItem value="last7days">
|
||||
最近7天
|
||||
</SelectItem>
|
||||
<SelectItem value="last30days">
|
||||
最近30天
|
||||
</SelectItem>
|
||||
<SelectItem value="last90days">
|
||||
最近90天
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -25,16 +35,22 @@
|
||||
<!-- 用户筛选(仅管理员可见) -->
|
||||
<Select
|
||||
v-if="isAdmin && availableUsers.length > 0"
|
||||
:model-value="filterUser"
|
||||
v-model:open="filterUserSelectOpen"
|
||||
:model-value="filterUser"
|
||||
@update:model-value="$emit('update:filterUser', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-36 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部用户" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">全部用户</SelectItem>
|
||||
<SelectItem v-for="user in availableUsers" :key="user.id" :value="user.id">
|
||||
<SelectItem value="__all__">
|
||||
全部用户
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="user in availableUsers"
|
||||
:key="user.id"
|
||||
:value="user.id"
|
||||
>
|
||||
{{ user.username || user.email }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -42,16 +58,22 @@
|
||||
|
||||
<!-- 模型筛选 -->
|
||||
<Select
|
||||
:model-value="filterModel"
|
||||
v-model:open="filterModelSelectOpen"
|
||||
:model-value="filterModel"
|
||||
@update:model-value="$emit('update:filterModel', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-40 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">全部模型</SelectItem>
|
||||
<SelectItem v-for="model in availableModels" :key="model" :value="model">
|
||||
<SelectItem value="__all__">
|
||||
全部模型
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="model in availableModels"
|
||||
:key="model"
|
||||
:value="model"
|
||||
>
|
||||
{{ model.replace('claude-', '') }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -59,16 +81,22 @@
|
||||
|
||||
<!-- 提供商筛选 -->
|
||||
<Select
|
||||
:model-value="filterProvider"
|
||||
v-model:open="filterProviderSelectOpen"
|
||||
:model-value="filterProvider"
|
||||
@update:model-value="$emit('update:filterProvider', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-32 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部提供商" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">全部提供商</SelectItem>
|
||||
<SelectItem v-for="provider in availableProviders" :key="provider" :value="provider">
|
||||
<SelectItem value="__all__">
|
||||
全部提供商
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="provider in availableProviders"
|
||||
:key="provider"
|
||||
:value="provider"
|
||||
>
|
||||
{{ provider }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -76,20 +104,32 @@
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<Select
|
||||
:model-value="filterStatus"
|
||||
v-model:open="filterStatusSelectOpen"
|
||||
:model-value="filterStatus"
|
||||
@update:model-value="$emit('update:filterStatus', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">全部状态</SelectItem>
|
||||
<SelectItem value="active">进行中</SelectItem>
|
||||
<SelectItem value="pending">等待中</SelectItem>
|
||||
<SelectItem value="streaming">流式传输</SelectItem>
|
||||
<SelectItem value="completed">已完成</SelectItem>
|
||||
<SelectItem value="failed">已失败</SelectItem>
|
||||
<SelectItem value="__all__">
|
||||
全部状态
|
||||
</SelectItem>
|
||||
<SelectItem value="active">
|
||||
进行中
|
||||
</SelectItem>
|
||||
<SelectItem value="pending">
|
||||
等待中
|
||||
</SelectItem>
|
||||
<SelectItem value="streaming">
|
||||
流式传输
|
||||
</SelectItem>
|
||||
<SelectItem value="completed">
|
||||
已完成
|
||||
</SelectItem>
|
||||
<SelectItem value="failed">
|
||||
已失败
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -97,58 +137,113 @@
|
||||
<div class="h-4 w-px bg-border" />
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<RefreshButton :loading="loading" @click="$emit('refresh')" />
|
||||
<RefreshButton
|
||||
:loading="loading"
|
||||
@click="$emit('refresh')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||
<TableHead class="h-12 font-semibold w-[70px]">时间</TableHead>
|
||||
<TableHead v-if="isAdmin" class="h-12 font-semibold w-[100px]">用户</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[140px]">模型</TableHead>
|
||||
<TableHead v-if="isAdmin" class="h-12 font-semibold w-[100px]">提供商</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[80px]">API格式</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[50px] text-center">类型</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[140px] text-right">Tokens</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[100px] text-right">费用</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[70px]">
|
||||
时间
|
||||
</TableHead>
|
||||
<TableHead
|
||||
v-if="isAdmin"
|
||||
class="h-12 font-semibold w-[100px]"
|
||||
>
|
||||
用户
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[140px]">
|
||||
模型
|
||||
</TableHead>
|
||||
<TableHead
|
||||
v-if="isAdmin"
|
||||
class="h-12 font-semibold w-[100px]"
|
||||
>
|
||||
提供商
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[80px]">
|
||||
API格式
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[50px] text-center">
|
||||
类型
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[140px] text-right">
|
||||
Tokens
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[100px] text-right">
|
||||
费用
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold w-[70px] text-right">
|
||||
<div class="inline-block max-w-[2rem] leading-tight">响应时间</div>
|
||||
<div class="inline-block max-w-[2rem] leading-tight">
|
||||
响应时间
|
||||
</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="records.length === 0">
|
||||
<TableCell :colspan="isAdmin ? 9 : 7" class="text-center py-12 text-muted-foreground">
|
||||
<TableCell
|
||||
:colspan="isAdmin ? 9 : 7"
|
||||
class="text-center py-12 text-muted-foreground"
|
||||
>
|
||||
暂无请求记录
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
v-else
|
||||
v-for="record in records"
|
||||
v-else
|
||||
:key="record.id"
|
||||
:class="isAdmin ? 'cursor-pointer border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]' : 'border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]'"
|
||||
@mousedown="handleMouseDown"
|
||||
@click="handleRowClick($event, record.id)"
|
||||
:class="isAdmin ? 'cursor-pointer border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]' : 'border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]'"
|
||||
>
|
||||
<TableCell class="text-xs py-4 w-[70px]">
|
||||
{{ formatDateTime(record.created_at) }}
|
||||
</TableCell>
|
||||
<TableCell v-if="isAdmin" class="py-4 w-[100px] truncate" :title="record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户')">
|
||||
<TableCell
|
||||
v-if="isAdmin"
|
||||
class="py-4 w-[100px] truncate"
|
||||
:title="record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户')"
|
||||
>
|
||||
{{ record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户') }}
|
||||
</TableCell>
|
||||
<TableCell class="font-medium py-4 w-[140px]" :title="getModelTooltip(record)">
|
||||
<div v-if="getActualModel(record)" class="flex flex-col text-xs gap-0.5">
|
||||
<TableCell
|
||||
class="font-medium py-4 w-[140px]"
|
||||
:title="getModelTooltip(record)"
|
||||
>
|
||||
<div
|
||||
v-if="getActualModel(record)"
|
||||
class="flex flex-col text-xs gap-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1 truncate">
|
||||
<span class="truncate">{{ record.model }}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 text-muted-foreground flex-shrink-0">
|
||||
<path fill-rule="evenodd" d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z" clip-rule="evenodd" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-3 h-3 text-muted-foreground flex-shrink-0"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-muted-foreground truncate">{{ getActualModel(record) }}</span>
|
||||
</div>
|
||||
<span v-else class="truncate block">{{ record.model }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="truncate block"
|
||||
>{{ record.model }}</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="isAdmin" class="py-4 w-[60px]">
|
||||
<TableCell
|
||||
v-if="isAdmin"
|
||||
class="py-4 w-[60px]"
|
||||
>
|
||||
<div class="flex flex-col text-xs gap-0.5">
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ record.provider }}</span>
|
||||
@@ -157,14 +252,30 @@
|
||||
class="inline-flex items-center justify-center w-4 h-4 text-xs text-amber-600 dark:text-amber-400"
|
||||
title="此请求发生了 Provider 切换"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
||||
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" clip-rule="evenodd" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="record.api_key_name" class="text-muted-foreground truncate" :title="record.api_key_name">
|
||||
<span
|
||||
v-if="record.api_key_name"
|
||||
class="text-muted-foreground truncate"
|
||||
:title="record.api_key_name"
|
||||
>
|
||||
{{ record.api_key_name }}
|
||||
<span v-if="record.rate_multiplier && record.rate_multiplier !== 1.0" class="text-foreground/60">({{ record.rate_multiplier }}x)</span>
|
||||
<span
|
||||
v-if="record.rate_multiplier && record.rate_multiplier !== 1.0"
|
||||
class="text-foreground/60"
|
||||
>({{ record.rate_multiplier }}x)</span>
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -176,23 +287,46 @@
|
||||
>
|
||||
{{ formatApiFormat(record.api_format) }}
|
||||
</span>
|
||||
<span v-else class="text-muted-foreground text-xs">-</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-muted-foreground text-xs"
|
||||
>-</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-center py-4 w-[50px]">
|
||||
<!-- 优先显示请求状态 -->
|
||||
<Badge v-if="record.status === 'pending'" variant="outline" class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground">
|
||||
<Badge
|
||||
v-if="record.status === 'pending'"
|
||||
variant="outline"
|
||||
class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground"
|
||||
>
|
||||
等待中
|
||||
</Badge>
|
||||
<Badge v-else-if="record.status === 'streaming'" variant="outline" class="whitespace-nowrap animate-pulse border-primary/50 text-primary">
|
||||
<Badge
|
||||
v-else-if="record.status === 'streaming'"
|
||||
variant="outline"
|
||||
class="whitespace-nowrap animate-pulse border-primary/50 text-primary"
|
||||
>
|
||||
传输中
|
||||
</Badge>
|
||||
<Badge v-else-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message" variant="destructive" class="whitespace-nowrap">
|
||||
<Badge
|
||||
v-else-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message"
|
||||
variant="destructive"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
失败
|
||||
</Badge>
|
||||
<Badge v-else-if="record.is_stream" variant="secondary" class="whitespace-nowrap">
|
||||
<Badge
|
||||
v-else-if="record.is_stream"
|
||||
variant="secondary"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
流式
|
||||
</Badge>
|
||||
<Badge v-else variant="outline" class="whitespace-nowrap border-border/60 text-muted-foreground">
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="whitespace-nowrap border-border/60 text-muted-foreground"
|
||||
>
|
||||
标准
|
||||
</Badge>
|
||||
</TableCell>
|
||||
@@ -213,7 +347,10 @@
|
||||
<TableCell class="text-right py-4 w-[100px]">
|
||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||
<span class="text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
|
||||
<span v-if="showActualCost && record.actual_cost !== undefined" class="text-muted-foreground">
|
||||
<span
|
||||
v-if="showActualCost && record.actual_cost !== undefined"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ formatCurrency(record.actual_cost) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -228,7 +365,10 @@
|
||||
<span v-else-if="record.response_time_ms">
|
||||
{{ (record.response_time_ms / 1000).toFixed(2) }}s
|
||||
</span>
|
||||
<span v-else class="text-muted-foreground">-</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-muted-foreground"
|
||||
>-</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
@@ -251,21 +391,23 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onUnmounted, watch } from 'vue'
|
||||
import TableCard from '@/components/ui/table-card.vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import Select from '@/components/ui/select.vue'
|
||||
import SelectTrigger from '@/components/ui/select-trigger.vue'
|
||||
import SelectValue from '@/components/ui/select-value.vue'
|
||||
import SelectContent from '@/components/ui/select-content.vue'
|
||||
import SelectItem from '@/components/ui/select-item.vue'
|
||||
import Table from '@/components/ui/table.vue'
|
||||
import TableHeader from '@/components/ui/table-header.vue'
|
||||
import TableBody from '@/components/ui/table-body.vue'
|
||||
import TableRow from '@/components/ui/table-row.vue'
|
||||
import TableHead from '@/components/ui/table-head.vue'
|
||||
import TableCell from '@/components/ui/table-cell.vue'
|
||||
import { Pagination, RefreshButton } from '@/components/ui'
|
||||
import {
|
||||
TableCard,
|
||||
Badge,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableCell,
|
||||
Pagination,
|
||||
RefreshButton,
|
||||
} from '@/components/ui'
|
||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||
import { formatDateTime } from '../composables'
|
||||
import { useRowClick } from '@/composables/useRowClick'
|
||||
|
||||
Reference in New Issue
Block a user