refactor: 统一 API 格式显示函数,补全 Usage/Trace 的 provider 链路信息

- 提取 formatApiFormat 为共享工具函数,替换各组件中分散的 API_FORMAT_LABELS 调用
- Trace API 返回密钥认证类型(key_auth_type)和 OAuth 套餐信息(key_oauth_plan_type)
- 请求时间线展示密钥认证方式标签(OAuth/Vertex AI/Kiro 等)
- Usage 记录补充 provider_id/endpoint_id/key_id,避免 curl 复现时缺失 provider 信息
- curl 复现兜底从 RequestCandidate 表查找 provider 信息
- Headers Diff 面板左右独立滚动并同步垂直滚动位置
This commit is contained in:
fawney19
2026-02-19 14:51:08 +08:00
parent 0d2cafaec3
commit 7a81e56553
23 changed files with 355 additions and 206 deletions

View File

@@ -67,6 +67,13 @@ export const API_FORMAT_ORDER: string[] = [
API_FORMATS.GEMINI_VIDEO,
]
// 工具函数:将 API 格式签名转为友好显示名称
export function formatApiFormat(format: string | null | undefined): string {
if (!format) return '-'
const raw = format.trim()
return API_FORMAT_LABELS[raw] || API_FORMAT_LABELS[raw.toLowerCase()] || API_FORMAT_LABELS[raw.toUpperCase()] || raw
}
// 工具函数:按标准顺序排序 API 格式数组
export function sortApiFormats(formats: string[]): string[] {
return [...formats].sort((a, b) => {

View File

@@ -12,7 +12,9 @@ export interface CandidateRecord {
endpoint_name?: string // 端点显示名称api_format
key_id?: string
key_name?: string // 密钥名称
key_preview?: string // 密钥脱敏预览(如 sk-***abc
key_preview?: string // 密钥脱敏预览(如 sk-***abcOAuth 类型不返回
key_auth_type?: string // 密钥认证类型api_key, oauth, vertex_ai 等)
key_oauth_plan_type?: string // OAuth 账号套餐类型free/plus/team/enterprise
key_capabilities?: Record<string, boolean> | null // Key 支持的能力
required_capabilities?: Record<string, boolean> | null // 请求实际需要的能力标签
status: 'pending' | 'streaming' | 'success' | 'failed' | 'skipped' | 'cancelled' | 'available' | 'unused' | 'stream_interrupted'

View File

@@ -84,7 +84,7 @@
variant="secondary"
class="text-xs font-semibold px-2.5 py-1"
>
{{ formatGroup.api_format }}
{{ formatApiFormat(formatGroup.api_format) }}
</Badge>
</div>
<div class="flex items-center gap-3">
@@ -604,6 +604,7 @@ import {
type RoutingEndpointInfo
} from '@/api/global-models'
import { API_FORMAT_ORDER } from '@/api/endpoints/types'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import { recoverKeyHealth } from '@/api/endpoints/health'
import { useToast } from '@/composables/useToast'
import { useCountdownTimer, getProbeCountdown } from '@/composables/useCountdownTimer'

View File

@@ -169,7 +169,7 @@
:key="fmt"
class="text-[10px] px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0"
>
{{ API_FORMAT_LABELS[fmt] || fmt }}
{{ formatApiFormat(fmt) }}
</span>
</div>
<p
@@ -250,9 +250,9 @@ import {
batchAssignModelsToProvider,
deleteModel,
importModelsFromUpstream,
API_FORMAT_LABELS,
type Model
} from '@/api/endpoints'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import { useUpstreamModelsCache, type UpstreamModel } from '../composables/useUpstreamModelsCache'
const props = defineProps<{

View File

@@ -26,7 +26,7 @@
<!-- 卡片头部格式名称 + 状态 + 操作 -->
<div class="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b">
<div class="flex items-center gap-3">
<span class="font-medium">{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span>
<span class="font-medium">{{ formatApiFormat(endpoint.api_format) }}</span>
<Badge
v-if="!endpoint.is_active"
variant="secondary"
@@ -737,7 +737,6 @@ import {
createEndpoint,
updateEndpoint,
deleteEndpoint,
API_FORMAT_LABELS,
type ProviderEndpoint,
type ProviderWithEndpointsSummary,
type HeaderRule,
@@ -747,6 +746,7 @@ import {
type BodyRuleConditionOp,
} from '@/api/endpoints'
import { adminApi } from '@/api/admin'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
// 编辑用的规则类型(统一的可编辑结构)
interface EditableRule {
@@ -1032,7 +1032,7 @@ const availableFormats = computed(() => {
// 删除确认弹窗描述
const deleteConfirmDescription = computed(() => {
if (!endpointToDelete.value) return ''
const formatLabel = API_FORMAT_LABELS[endpointToDelete.value.api_format] || endpointToDelete.value.api_format
const formatLabel = formatApiFormat(endpointToDelete.value.api_format)
return `确定要删除 ${formatLabel} 端点吗?关联密钥将移除对该 API 格式的支持。`
})
@@ -2040,7 +2040,7 @@ async function handleAddEndpoint() {
custom_path: newEndpoint.value.custom_path || undefined,
is_active: true,
})
success(`已添加 ${API_FORMAT_LABELS[newEndpoint.value.api_format] || newEndpoint.value.api_format} 端点`)
success(`已添加 ${formatApiFormat(newEndpoint.value.api_format)} 端点`)
// 重置表单,保留 URL
newEndpoint.value = { api_format: '', base_url: baseUrl, custom_path: '' }
emit('endpointCreated')
@@ -2082,7 +2082,7 @@ async function confirmDeleteEndpoint() {
try {
await deleteEndpoint(endpoint.id)
success(`已删除 ${API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format} 端点`)
success(`已删除 ${formatApiFormat(endpoint.api_format)} 端点`)
emit('endpointUpdated')
} catch (error: any) {
showError(error.response?.data?.detail || '删除失败', '错误')

View File

@@ -83,7 +83,7 @@
variant="outline"
class="font-mono text-xs"
>
{{ monitor.api_format }}
{{ formatApiFormat(monitor.api_format) }}
</Badge>
<Badge
v-if="monitor.total_attempts > 0"
@@ -142,6 +142,7 @@ import EndpointHealthTimeline from './EndpointHealthTimeline.vue'
import { getEndpointStatusMonitor, getPublicEndpointStatusMonitor } from '@/api/endpoints/health'
import type { EndpointStatusMonitor, PublicEndpointStatusMonitor } from '@/api/endpoints/types'
import { useToast } from '@/composables/useToast'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
const props = withDefaults(defineProps<{
title?: string

View File

@@ -126,7 +126,7 @@
variant="outline"
class="text-[10px] px-1.5 py-0 shrink-0"
>
{{ API_FORMAT_LABELS[fmt] || fmt }}
{{ formatApiFormat(fmt) }}
</Badge>
<Badge
v-if="isModelExisting(model.id)"
@@ -197,8 +197,8 @@ import {
getProviderModels,
type EndpointAPIKey,
type UpstreamModel,
API_FORMAT_LABELS,
} from '@/api/endpoints'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache'
const props = defineProps<{

View File

@@ -125,7 +125,7 @@
<span
class="text-sm whitespace-nowrap"
:class="form.api_formats.includes(format) ? 'text-primary' : 'text-muted-foreground'"
>{{ API_FORMAT_LABELS[format] || format }}</span>
>{{ formatApiFormat(format) }}</span>
</div>
<div
class="flex items-center shrink-0 ml-2 text-xs text-muted-foreground gap-1"
@@ -321,7 +321,6 @@ import {
addProviderKey,
updateProviderKey,
getAllCapabilities,
API_FORMAT_LABELS,
sortApiFormats,
type EndpointAPIKey,
type EndpointAPIKeyUpdate,
@@ -329,6 +328,7 @@ import {
type CapabilityDefinition,
type ProviderType
} from '@/api/endpoints'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
const props = defineProps<{
open: boolean

View File

@@ -188,7 +188,7 @@
]"
@click="activeFormatTab = format"
>
{{ API_FORMAT_LABELS[format] || format }}
{{ formatApiFormat(format) }}
</button>
</div>
@@ -436,7 +436,8 @@ import { updateProvider, updateProviderKey } from '@/api/endpoints'
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
import { adminApi } from '@/api/admin'
import { batchQueryBalance, type ActionResultResponse, type BalanceInfo } from '@/api/providerOps'
import { API_FORMAT_SHORT, API_FORMAT_LABELS } from '@/api/endpoints/types'
import { API_FORMAT_SHORT } from '@/api/endpoints/types'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
interface KeyWithMeta {
id: string

View File

@@ -98,9 +98,9 @@
<span
class="text-xs px-2 py-0.5 rounded-md border border-border bg-background hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors font-medium"
:class="{ 'opacity-40': !endpoint.is_active }"
:title="`编辑 ${API_FORMAT_LABELS[endpoint.api_format]} 端点`"
:title="`编辑 ${formatApiFormat(endpoint.api_format)} 端点`"
@click="handleEditEndpoint(endpoint)"
>{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span>
>{{ formatApiFormat(endpoint.api_format) }}</span>
</template>
<span
v-if="endpoints.length > 0"
@@ -1053,12 +1053,12 @@ import {
type ProviderEndpoint,
type EndpointAPIKey,
type Model,
API_FORMAT_LABELS,
API_FORMAT_ORDER,
API_FORMAT_SHORT,
sortApiFormats,
} from '@/api/endpoints'
import type { UpstreamMetadata, AntigravityModelQuota } from '@/api/endpoints/types'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
// 扩展端点类型,包含密钥列表
interface ProviderEndpointWithKeys extends ProviderEndpoint {

View File

@@ -67,7 +67,7 @@
variant="outline"
class="text-xs"
>
{{ API_FORMAT_LABELS[format] || format }}
{{ formatApiFormat(format) }}
</Badge>
</div>
<!-- 映射数量 -->
@@ -201,6 +201,7 @@ import {
} from '@/api/endpoints'
import { updateModel } from '@/api/endpoints/models'
import { parseTestModelError } from '@/utils/errorParser'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
const props = defineProps<{
provider: any
@@ -296,7 +297,7 @@ const deleteConfirmDescription = computed(() => {
if (!deletingGroup.value) return ''
const { model, aliases, apiFormats } = deletingGroup.value
const modelName = model.global_model_display_name || model.provider_model_name
const scopeText = apiFormats.length === 0 ? '全部' : apiFormats.map(f => API_FORMAT_LABELS[f] || f).join(', ')
const scopeText = apiFormats.length === 0 ? '全部' : apiFormats.map(f => formatApiFormat(f)).join(', ')
const aliasNames = aliases.map(a => a.name).join(', ')
return `确定要删除模型「${modelName}」在作用域「${scopeText}」下的 ${aliases.length} 个映射吗?\n\n映射名称${aliasNames}`
})

View File

@@ -92,14 +92,6 @@
</div>
</div>
<!-- 格式转换标记节点下方 -->
<div
v-if="group.hasConversion"
class="conversion-indicator"
>
{{ group.primary.extra_data?.provider_api_format || '转换' }}
</div>
<!-- 连接线 -->
<div
v-if="groupIndex < groupedTimeline.length - 1"
@@ -202,25 +194,31 @@
<span class="info-value mono">{{ formatLatency(currentAttempt.extra_data.first_byte_time_ms) }}</span>
</div>
<div
v-if="currentAttempt.extra_data?.needs_conversion"
v-if="currentAttempt.extra_data?.provider_api_format"
class="info-item"
>
<span class="info-label">格式</span>
<span class="info-value">
<span class="conversion-badge">格式转换</span>
<code
v-if="currentAttempt.extra_data?.provider_api_format"
class="ml-1.5 text-xs"
>{{ currentAttempt.extra_data.provider_api_format }}</code>
<code class="format-code">{{ formatApiFormat(currentAttempt.extra_data.provider_api_format) }}</code>
<span
v-if="currentAttempt.extra_data?.needs_conversion"
class="conversion-badge ml-1.5"
>格式转换</span>
</span>
</div>
<div
v-if="currentAttempt.key_name || currentAttempt.key_id"
class="info-item"
>
<span class="info-label">密钥</span>
<span class="info-label">{{ isOAuthType(currentAttempt.key_auth_type) ? '账号' : '密钥' }}</span>
<span class="info-value info-value-stacked">
<span class="key-name">{{ currentAttempt.key_name || '未知' }}</span>
<span class="key-name">
{{ currentAttempt.key_name || '未知' }}
<span
v-if="currentAttempt.key_auth_type && currentAttempt.key_auth_type !== 'api_key'"
class="auth-type-tag"
>{{ formatAuthTypeWithPlan(currentAttempt.key_auth_type, currentAttempt.key_oauth_plan_type) }}</span>
</span>
<code
v-if="currentAttempt.key_preview"
class="key-preview"
@@ -373,6 +371,7 @@ import Skeleton from '@/components/ui/skeleton.vue'
import { ChevronLeft, ChevronRight, ExternalLink } from 'lucide-vue-next'
import { requestTraceApi, type RequestTrace, type CandidateRecord } from '@/api/requestTrace'
import { log } from '@/utils/logger'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
// 节点组类型
interface NodeGroup {
@@ -386,6 +385,7 @@ interface NodeGroup {
startIndex: number
endIndex: number
hasConversion: boolean // 组内是否有格式转换候选
providerApiFormat: string | null // 提供商 API 格式(如 openai:cli
}
// 用量数据类型
@@ -615,6 +615,7 @@ const groupedTimeline = computed<NodeGroup[]>(() => {
startIndex: index,
endIndex: index,
hasConversion: candidate.extra_data?.needs_conversion === true,
providerApiFormat: candidate.extra_data?.provider_api_format || null,
}
groups.push(currentGroup)
}
@@ -722,6 +723,30 @@ const isCapabilityUsed = (cap: string): boolean => {
return activeCapabilities.value.includes(cap)
}
// 判断是否为 OAuth 类型provider_type 为具体值时也算 OAuth
const isOAuthType = (authType?: string): boolean => {
if (!authType) return false
return !['api_key', 'vertex_ai'].includes(authType)
}
// 格式化认证类型(合并 plan 信息,避免冗余)
const formatAuthTypeWithPlan = (authType: string, planType?: string): string => {
const labels: Record<string, string> = {
'oauth': 'OAuth',
'vertex_ai': 'Vertex AI',
'kiro': 'Kiro',
'codex': 'Codex',
'antigravity': 'Antigravity',
'claude_code': 'Claude Code',
'gemini_cli': 'Gemini CLI',
}
const typeName = labels[authType] || authType
if (planType) {
return `${typeName} ${planType}`
}
return typeName
}
// 格式化能力标签显示
const formatCapabilityLabel = (cap: string): string => {
const labels: Record<string, string> = {
@@ -1143,24 +1168,6 @@ const getStatusColorClass = (status: string) => {
.node-dot.status-skipped { color: hsl(var(--primary)); }
.node-dot.status-available { color: #d1d5db; }
/* 格式转换标记(节点下方) */
.conversion-indicator {
position: absolute;
top: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
font-size: 0.55rem;
color: hsl(var(--muted-foreground) / 0.7);
white-space: nowrap;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
padding: 1px 4px;
border: 1px dashed hsl(var(--border));
border-radius: 3px;
background: hsl(var(--muted) / 0.3);
}
/* 连接线容器 */
.node-line-wrapper {
position: absolute;
@@ -1426,6 +1433,16 @@ const getStatusColorClass = (status: string) => {
gap: 0.2rem;
}
/* 格式代码 */
.format-code {
font-size: 0.75rem;
padding: 0.1rem 0.3rem;
background: hsl(var(--muted));
border-radius: 3px;
color: hsl(var(--muted-foreground));
font-family: ui-monospace, monospace;
}
/* Key 信息 */
.key-name {
font-weight: 500;
@@ -1440,6 +1457,20 @@ const getStatusColorClass = (status: string) => {
font-family: ui-monospace, monospace;
}
/* 认证类型标签 */
.auth-type-tag {
display: inline-flex;
align-items: center;
padding: 0.1rem 0.35rem;
margin-left: 0.375rem;
font-size: 0.65rem;
font-weight: 500;
color: hsl(var(--primary) / 0.8);
background: hsl(var(--primary) / 0.08);
border: 1px solid hsl(var(--primary) / 0.2);
border-radius: 3px;
}
/* 代理信息 */
.proxy-name {
font-weight: 500;

View File

@@ -640,7 +640,7 @@ import Tabs from '@/components/ui/tabs.vue'
import TabsContent from '@/components/ui/tabs-content.vue'
import { Copy, Check, Maximize2, Minimize2, Columns2, RefreshCw, X, Monitor, Server, MessageSquareText, Code2, Terminal, Play } from 'lucide-vue-next'
import { dashboardApi, type RequestDetail } from '@/api/dashboard'
import { API_FORMAT_LABELS } from '@/api/endpoints/types'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import { log } from '@/utils/logger'
// 子组件
@@ -1074,17 +1074,6 @@ function getTaskTypeLabel(taskType: string): string {
}
}
function formatApiFormat(format: string | null | undefined): string {
if (!format) return '-'
const raw = (format || '').trim()
return (
API_FORMAT_LABELS[raw] ||
API_FORMAT_LABELS[raw.toLowerCase()] ||
API_FORMAT_LABELS[raw.toUpperCase()] ||
raw
)
}
function formatNumber(num: number): string {
if (num >= 1_000_000) {
return `${(num / 1_000_000).toFixed(1) }M`

View File

@@ -25,94 +25,100 @@
</div>
<!-- 并排 Diff 内容 -->
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<div class="flex font-mono text-xs">
<!-- 左侧客户端 -->
<div class="flex-1 border-r">
<template
v-for="entry in sortedEntries"
:key="'left-' + entry.key"
<div class="flex font-mono text-xs max-h-[500px]">
<!-- 左侧客户端 -->
<div
ref="leftPanelRef"
class="w-1/2 min-w-0 border-r overflow-x-auto overflow-y-auto"
@scroll="onLeftScroll"
>
<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"
>
<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"
>
<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"
>
<span class="text-muted-foreground">
"{{ entry.key }}": "{{ entry.clientValue }}"
</span>
</div>
</template>
</div>
<!-- 右侧提供商 -->
<div class="flex-1">
<template
v-for="entry in sortedEntries"
:key="'right-' + entry.key"
<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-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"
>
<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"
>
<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"
>
<span class="text-muted-foreground">
"{{ entry.key }}": "{{ entry.providerValue }}"
</span>
</div>
</template>
</div>
<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"
>
<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"
>
<span class="text-muted-foreground">
"{{ entry.key }}": "{{ entry.clientValue }}"
</span>
</div>
</template>
</div>
<!-- 右侧提供商 -->
<div
ref="rightPanelRef"
class="w-1/2 min-w-0 overflow-x-auto overflow-y-auto"
@scroll="onRightScroll"
>
<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"
>
<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"
>
<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"
>
<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"
>
<span class="text-muted-foreground">
"{{ entry.key }}": "{{ entry.providerValue }}"
</span>
</div>
</template>
</div>
</div>
</Card>
@@ -150,7 +156,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref } from 'vue'
import Card from '@/components/ui/card.vue'
import JsonContent from './JsonContent.vue'
import type { RequestDetail } from '@/api/dashboard'
@@ -168,6 +174,28 @@ const props = defineProps<{
isDark: boolean
}>()
const leftPanelRef = ref<HTMLElement | null>(null)
const rightPanelRef = ref<HTMLElement | null>(null)
let isSyncingScroll = false
function onLeftScroll() {
if (isSyncingScroll) return
isSyncingScroll = true
if (leftPanelRef.value && rightPanelRef.value) {
rightPanelRef.value.scrollTop = leftPanelRef.value.scrollTop
}
requestAnimationFrame(() => { isSyncingScroll = false })
}
function onRightScroll() {
if (isSyncingScroll) return
isSyncingScroll = true
if (leftPanelRef.value && rightPanelRef.value) {
leftPanelRef.value.scrollTop = rightPanelRef.value.scrollTop
}
requestAnimationFrame(() => { isSyncingScroll = false })
}
// 合并并排序的条目(用于并排显示)
const sortedEntries = computed(() => {
const clientHeaders = props.detail.request_headers || {}

View File

@@ -78,7 +78,7 @@ 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 { formatTokens, formatCurrency } from '@/utils/format'
import { API_FORMAT_LABELS } from '@/api/endpoints/types'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import type { ApiFormatStatsItem } from '../types'
defineProps<{
@@ -86,15 +86,4 @@ defineProps<{
isAdmin: boolean
}>()
// 格式化 API 格式显示名称
function formatApiFormat(format: string): string {
const raw = (format || '').trim()
return (
API_FORMAT_LABELS[raw] ||
API_FORMAT_LABELS[raw.toLowerCase()] ||
API_FORMAT_LABELS[raw.toUpperCase()] ||
raw
)
}
</script>

View File

@@ -668,7 +668,7 @@ import { RefreshCcw, Search } from 'lucide-vue-next'
import { formatTokens, formatCurrency } from '@/utils/format'
import { formatDateTime } from '../composables'
import { useRowClick } from '@/composables/useRowClick'
import { API_FORMAT_LABELS } from '@/api/endpoints/types'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import type { DateRangeParams, UsageRecord } from '../types'
import { TimeRangePicker } from '@/components/common'
@@ -811,17 +811,6 @@ function handleRowClick(event: MouseEvent, id: string) {
// useIntervalFn 和 useDebounceFn 自动处理清理,无需 onUnmounted
// 格式化 API 格式显示名称
function formatApiFormat(format: string): string {
const raw = (format || '').trim()
return (
API_FORMAT_LABELS[raw] ||
API_FORMAT_LABELS[raw.toLowerCase()] ||
API_FORMAT_LABELS[raw.toUpperCase()] ||
raw
)
}
// 判断是否应该显示格式转换信息
// 包括1. 跨格式转换has_format_conversion=true2. 同族格式差异(如 CLAUDE_CLI → CLAUDE
function shouldShowFormatConversion(record: UsageRecord): boolean {

View File

@@ -32,6 +32,7 @@ import {
getFrequencyClass
} from '@/composables/useTTLAnalysis'
import { log } from '@/utils/logger'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
// ==================== 缓存统计与亲和性列表 ====================
@@ -660,7 +661,7 @@ onBeforeUnmount(() => {
</TableCell>
<TableCell>
<div class="text-sm">
{{ item.api_format || '---' }}
{{ formatApiFormat(item.api_format) }}
</div>
<div class="text-xs text-muted-foreground font-mono">
{{ item.key_prefix || '---' }}
@@ -740,7 +741,7 @@ onBeforeUnmount(() => {
<span class="truncate max-w-[100px]">{{ item.model_display_name || '---' }}</span>
</div>
<div class="flex items-center justify-between text-xs">
<span class="text-muted-foreground">{{ item.api_format || '---' }}</span>
<span class="text-muted-foreground">{{ formatApiFormat(item.api_format) }}</span>
<span>{{ getRemainingTime(item.expire_at) }} · {{ item.request_count }}</span>
</div>
</div>