improve(log):优化管理后台请求日志显示效果
This commit is contained in:
1 parent
0964d9014b
commit
483ffded09
2 files changed
+322
-374
No files matched your search
@@ -128,7 +128,7 @@ function goToTask(taskId: string) {
|
||||
}
|
||||
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 100 },
|
||||
{ title: '时间', dataIndex: 'created_at', width: 180 },
|
||||
{ title: '租户', dataIndex: 'tenant_name', width: 120, ellipsis: true, render({ record }) {
|
||||
return record.tenant_name || record.tenant_id
|
||||
}},
|
||||
@@ -180,7 +180,6 @@ const columns: TableColumnData[] = [
|
||||
return h(Tag, { color: auditLevelTagColor[record.audit_level] || 'gray', size: 'small' }, () => auditLevelLabel[record.audit_level] || record.audit_level)
|
||||
},
|
||||
},
|
||||
{ title: '时间', dataIndex: 'created_at', width: 180 },
|
||||
{
|
||||
title: '操作', dataIndex: 'actions', width: 80, fixed: 'right',
|
||||
render({ record }) {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
Message, Tag, Tooltip, Space, Button,
|
||||
} from '@arco-design/web-vue'
|
||||
import { IconArchive, IconSave } from '@arco-design/web-vue/es/icon'
|
||||
import { IconSave, IconFile, IconBarChart, IconCamera, IconExclamationCircle } from '@arco-design/web-vue/es/icon'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ForwardingTracePanel from '@/components/ForwardingTracePanel.vue'
|
||||
import request from '@/utils/request'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
@@ -160,6 +159,8 @@ const billingSourceLabel: Record<string, string> = {
|
||||
}
|
||||
|
||||
const detailVisible = ref(false)
|
||||
// 计费快照默认折叠,需要时展开查看定价/倍率等审计细节
|
||||
const snapshotCollapsed = ref(true)
|
||||
const detailLog = ref<any>(null)
|
||||
const router = useRouter()
|
||||
|
||||
@@ -173,6 +174,25 @@ function formatMs(n: number): string {
|
||||
return n < 1000 ? `${n}ms` : `${(n / 1000).toFixed(2)}s`
|
||||
}
|
||||
|
||||
// 耗时分级着色:按三档阈值返回 绿/蓝/橙,超出最大档返回红色(输入毫秒)
|
||||
function durationColor(ms: number, thresholds: [number, number, number]): string {
|
||||
if (ms == null || ms <= 0) return 'inherit'
|
||||
if (ms < thresholds[0]) return '#00b42a' // 绿色:优
|
||||
if (ms < thresholds[1]) return '#165dff' // 蓝色:良
|
||||
if (ms < thresholds[2]) return '#ff7d00' // 橙色:中
|
||||
return '#f53f3f' // 红色:差
|
||||
}
|
||||
|
||||
// 延迟分级:<30s 绿 / <60s 蓝 / <180s 橙 / ≥180s 红
|
||||
function latencyColor(ms: number): string {
|
||||
return durationColor(ms, [30000, 60000, 180000])
|
||||
}
|
||||
|
||||
// TTFT 分级:<3s 绿 / <10s 蓝 / <30s 橙 / ≥30s 红
|
||||
function ttftColor(ms: number): string {
|
||||
return durationColor(ms, [3000, 10000, 30000])
|
||||
}
|
||||
|
||||
function formatTime(s: string): string {
|
||||
if (!s) return '-'
|
||||
return s.replace('T', ' ').substring(0, 19)
|
||||
@@ -203,7 +223,7 @@ function totalTokens(log: any): number {
|
||||
}
|
||||
|
||||
function parseSnapshot(log: any): any {
|
||||
if (!log.billing_snapshot) return null
|
||||
if (!log?.billing_snapshot) return null
|
||||
try {
|
||||
return typeof log.billing_snapshot === 'string' ? JSON.parse(log.billing_snapshot) : log.billing_snapshot
|
||||
} catch {
|
||||
@@ -211,6 +231,77 @@ function parseSnapshot(log: any): any {
|
||||
}
|
||||
}
|
||||
|
||||
// 计费快照解析结果:computed 只解析一次,避免模板里几十处调用反复 JSON.parse
|
||||
const snapshot = computed(() => parseSnapshot(detailLog.value))
|
||||
|
||||
// 快照 token_costs 的 key → 中文标签
|
||||
const tokenCostLabels: Record<string, string> = {
|
||||
input: '输入',
|
||||
output: '输出',
|
||||
cache_read: '缓存读取',
|
||||
cache_creation: '缓存创建',
|
||||
cache_creation_5m: '缓存创建(5分钟)',
|
||||
cache_creation_1h: '缓存创建(1小时)',
|
||||
}
|
||||
|
||||
// 消费明细小票行:divider=true 渲染分隔线,strong=true 渲染小计行,total=true 渲染合计行
|
||||
interface ReceiptRow {
|
||||
label?: string
|
||||
value?: string
|
||||
sub?: string
|
||||
valueClass?: string
|
||||
strong?: boolean
|
||||
total?: boolean
|
||||
divider?: boolean
|
||||
}
|
||||
|
||||
function tokenRow(label: string, tokens: number | null | undefined): ReceiptRow {
|
||||
return { label, value: (tokens || 0).toLocaleString() }
|
||||
}
|
||||
|
||||
// 小票行数据驱动生成,替代手写重复模板
|
||||
const receiptRows = computed<ReceiptRow[]>(() => {
|
||||
const d = detailLog.value
|
||||
if (!d) return []
|
||||
const rows: ReceiptRow[] = [
|
||||
tokenRow('输入 Token', d.input_tokens),
|
||||
tokenRow('输出 Token', d.output_tokens),
|
||||
]
|
||||
if ((d.cache_read_tokens || 0) > 0) rows.push(tokenRow('缓存读取', d.cache_read_tokens))
|
||||
if ((d.cache_creation_tokens || 0) > 0) rows.push(tokenRow('缓存创建', d.cache_creation_tokens))
|
||||
if ((d.cache_creation_5m_tokens || 0) > 0) rows.push(tokenRow('缓存创建(5分钟)', d.cache_creation_5m_tokens))
|
||||
if ((d.cache_creation_1h_tokens || 0) > 0) rows.push(tokenRow('缓存创建(1小时)', d.cache_creation_1h_tokens))
|
||||
if ((d.reasoning_tokens || 0) > 0) rows.push(tokenRow('推理 Token', d.reasoning_tokens))
|
||||
if ((d.audio_input_tokens || 0) > 0) rows.push(tokenRow('音频输入', d.audio_input_tokens))
|
||||
if ((d.audio_output_tokens || 0) > 0) rows.push(tokenRow('音频输出', d.audio_output_tokens))
|
||||
if ((d.image_output_tokens || 0) > 0) rows.push(tokenRow('图像输出', d.image_output_tokens))
|
||||
if ((d.image_count || 0) > 0) rows.push({ label: '生成图片', value: `${d.image_count} 张`, sub: d.image_size ? `(${d.image_size})` : '' })
|
||||
// Token 合计行
|
||||
rows.push({ label: 'Token 合计', value: totalTokens(d).toLocaleString(), strong: true })
|
||||
rows.push({ divider: true })
|
||||
rows.push({ label: '输入费用', value: formatCost(d.input_cost || 0) })
|
||||
rows.push({ label: '输出费用', value: formatCost(d.output_cost || 0) })
|
||||
if ((d.cache_creation_cost || 0) > 0) rows.push({ label: '缓存创建费用', value: formatCost(d.cache_creation_cost) })
|
||||
if ((d.cache_read_cost || 0) > 0) rows.push({ label: '缓存读取费用', value: formatCost(d.cache_read_cost) })
|
||||
const rm = d.rate_multiplier
|
||||
rows.push({
|
||||
label: '费率倍率',
|
||||
value: rm && rm !== 1 ? `${rm.toFixed(4)}x` : '-',
|
||||
valueClass: rm && rm !== 1 ? (rm < 1 ? 'text-success' : 'text-warning') : undefined,
|
||||
})
|
||||
rows.push({ label: '基础费用', value: formatCost(d.total_cost || 0) })
|
||||
rows.push({ label: '定价来源', value: billingSourceLabel[d.billing_source] || d.billing_source || '-' })
|
||||
if ((d.pre_deduct_amount || 0) > 0 || (d.refund_amount || 0) > 0 || (d.supplement_amount || 0) > 0) {
|
||||
rows.push({ divider: true })
|
||||
rows.push({ label: '预扣金额', value: formatCost(d.pre_deduct_amount) })
|
||||
if ((d.refund_amount || 0) > 0) rows.push({ label: '退回金额', value: formatCost(d.refund_amount), valueClass: 'text-success' })
|
||||
if ((d.supplement_amount || 0) > 0) rows.push({ label: '补扣金额', value: formatCost(d.supplement_amount), valueClass: 'text-warning' })
|
||||
}
|
||||
rows.push({ divider: true })
|
||||
rows.push({ label: '实际费用', value: formatCost(d.actual_cost || 0), total: true })
|
||||
return rows
|
||||
})
|
||||
|
||||
function copyText(text: string) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
Message.success('已复制')
|
||||
@@ -224,32 +315,11 @@ function viewAuditLog(requestId: string, taskId?: string) {
|
||||
router.push({ name: 'AdminRequestAuditLogs', query })
|
||||
}
|
||||
|
||||
// 转发路径追踪(从审计日志按 request_id 懒加载,用量日志表不存 trace)
|
||||
const forwardingTrace = ref<any>(null)
|
||||
const forwardingTraceLoading = ref(false)
|
||||
const forwardingTraceFound = ref(false)
|
||||
|
||||
async function fetchForwardingTrace(requestId: string) {
|
||||
forwardingTrace.value = null
|
||||
forwardingTraceFound.value = false
|
||||
if (!requestId) return
|
||||
forwardingTraceLoading.value = true
|
||||
try {
|
||||
const res: any = await request.get(`/admin/audit/forwarding-trace/${encodeURIComponent(requestId)}`)
|
||||
const d = res.data?.data
|
||||
forwardingTraceFound.value = !!d?.found
|
||||
forwardingTrace.value = d?.forwarding_trace || null
|
||||
} catch {
|
||||
/* 静默:trace 为辅助信息,失败不打扰用户 */
|
||||
} finally {
|
||||
forwardingTraceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openDetail(record: any) {
|
||||
detailLog.value = record
|
||||
// 每次打开重置为折叠,避免上一条记录的快照展开状态带入
|
||||
snapshotCollapsed.value = true
|
||||
detailVisible.value = true
|
||||
fetchForwardingTrace(record.request_id)
|
||||
}
|
||||
|
||||
function tooltipRow(label: string, value: string, valueClass = 'dark-tooltip-value') {
|
||||
@@ -260,7 +330,12 @@ function tooltipRow(label: string, value: string, valueClass = 'dark-tooltip-val
|
||||
}
|
||||
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 100 },
|
||||
{
|
||||
title: '时间', dataIndex: 'created_at', width: 160,
|
||||
render({ record }) {
|
||||
return h('span', { style: 'white-space: nowrap' }, formatTime(record.created_at))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '租户', dataIndex: 'tenant_name', width: 120, ellipsis: true,
|
||||
render({ record }) { return record.tenant_name || record.tenant_id },
|
||||
@@ -338,14 +413,15 @@ const columns: TableColumnData[] = [
|
||||
h('span', { class: 'token-symbol', 'aria-hidden': 'true' }, '↓'),
|
||||
h('span', { class: 'token-value' }, (record.output_tokens || 0).toLocaleString()),
|
||||
]),
|
||||
h('span', { class: 'token-item', style: 'color: #0fc6c2', 'aria-label': `缓存读取 ${(record.cache_read_tokens || 0).toLocaleString()}` }, [
|
||||
h('span', { class: 'token-symbol', 'aria-hidden': 'true' }, '⚡'),
|
||||
h('span', { class: 'token-value' }, (record.cache_read_tokens || 0).toLocaleString()),
|
||||
]),
|
||||
h('span', { class: 'token-item', style: 'color: #ff7d00', 'aria-label': `缓存写入 ${(record.cache_creation_tokens || 0).toLocaleString()}` }, [
|
||||
h(IconSave, { size: 13, 'aria-hidden': 'true' }),
|
||||
h('span', { class: 'token-value' }, (record.cache_creation_tokens || 0).toLocaleString()),
|
||||
]),
|
||||
h('span', { class: 'token-item', style: 'color: #0fc6c2', 'aria-label': `缓存读取 ${(record.cache_read_tokens || 0).toLocaleString()}` }, [
|
||||
h('span', { class: 'token-symbol', 'aria-hidden': 'true' }, '⚡'),
|
||||
h('span', { class: 'token-value' }, (record.cache_read_tokens || 0).toLocaleString()),
|
||||
]),
|
||||
|
||||
]),
|
||||
]),
|
||||
content: () => h('div', { class: 'dark-tooltip' }, tooltipContent),
|
||||
@@ -391,16 +467,16 @@ const columns: TableColumnData[] = [
|
||||
title: '延迟', dataIndex: 'latency_ms', width: 100,
|
||||
render({ record }) {
|
||||
const items: any[] = [
|
||||
h('div', { class: 'time-text' }, formatMs(record.latency_ms)),
|
||||
h('div', { class: 'time-text', style: { color: latencyColor(record.latency_ms) } }, formatMs(record.latency_ms)),
|
||||
]
|
||||
if (record.first_token_ms > 0) {
|
||||
items.push(h('div', { class: 'sub-text', style: 'font-size: 11px' }, `TTFT ${formatMs(record.first_token_ms)}`))
|
||||
items.push(h('div', { class: 'sub-text', style: { fontSize: '11px', color: ttftColor(record.first_token_ms) } }, `TTFT ${formatMs(record.first_token_ms)}`))
|
||||
}
|
||||
return h('div', { style: 'line-height: 1.4' }, items)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'status', width: 80,
|
||||
title: '状态', dataIndex: 'status', width: 130,
|
||||
render({ record }) {
|
||||
const items: any[] = [
|
||||
h(Tag, { color: statusTagColor[record.status], size: 'small' }, () => statusLabel[record.status] || record.status),
|
||||
@@ -411,12 +487,6 @@ const columns: TableColumnData[] = [
|
||||
return h(Space, { size: 4 }, () => items)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '时间', dataIndex: 'created_at', width: 160,
|
||||
render({ record }) {
|
||||
return h('span', { class: 'time-text' }, formatTime(record.created_at))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作', dataIndex: 'actions', width: 80, fixed: 'right',
|
||||
render({ record }) {
|
||||
@@ -715,18 +785,47 @@ const { exporting, exportFile } = useExport({
|
||||
<a-modal
|
||||
v-model:visible="detailVisible"
|
||||
title="用量详情"
|
||||
:width="720"
|
||||
:width="isMobileView ? '92%' : 720"
|
||||
:footer="false"
|
||||
:body-style="{ maxHeight: '65vh', overflowY: 'auto' }"
|
||||
unmount-on-close
|
||||
>
|
||||
<template v-if="detailLog">
|
||||
<!-- 关键结果摘要:模型 / 状态 / 费用 / 耗时一眼可读 -->
|
||||
<div class="detail-summary">
|
||||
<div class="detail-summary-main">
|
||||
<span class="summary-model">{{ detailLog.model_name }}<span v-if="hasUpstreamModel(detailLog)" class="summary-upstream"> ↳ {{ detailLog.upstream_model }}</span></span>
|
||||
<a-tag :color="statusTagColor[detailLog.status]" size="small">{{ statusLabel[detailLog.status] || detailLog.status }}</a-tag>
|
||||
<span v-if="detailLog.retry_index > 0" class="retry-badge">重试 {{ detailLog.retry_index }} 次</span>
|
||||
<span class="summary-cost">{{ formatCost(detailLog.actual_cost || 0) }}</span>
|
||||
</div>
|
||||
<div class="detail-summary-sub">
|
||||
<span :style="{ color: latencyColor(detailLog.latency_ms) }">总延迟 {{ formatMs(detailLog.latency_ms) }}</span>
|
||||
<template v-if="detailLog.first_token_ms > 0">
|
||||
<span class="summary-dot">·</span>
|
||||
<span :style="{ color: ttftColor(detailLog.first_token_ms) }">首 Token {{ formatMs(detailLog.first_token_ms) }}</span>
|
||||
</template>
|
||||
<span class="summary-dot">·</span>
|
||||
<span>{{ formatTime(detailLog.created_at) }}</span>
|
||||
<a-tag v-if="detailLog.stream_end_reason" :color="detailLog.stream_end_reason === 'done' ? 'green' : 'gray'" size="small">{{ detailLog.stream_end_reason }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detailLog.error_message" class="detail-section">
|
||||
<div class="detail-section-title error-title">
|
||||
<span class="detail-icon detail-icon-error"><IconExclamationCircle /></span>
|
||||
错误信息
|
||||
</div>
|
||||
<div class="error-message">{{ detailLog.error_message }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">
|
||||
<span class="detail-icon detail-icon-doc"></span>
|
||||
<span class="detail-icon"><IconFile /></span>
|
||||
基本信息
|
||||
</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<div class="detail-item detail-item-full">
|
||||
<span class="detail-label">请求 ID</span>
|
||||
<span class="detail-value mono-text">
|
||||
{{ detailLog.request_id }}
|
||||
@@ -734,7 +833,7 @@ const { exporting, exportFile } = useExport({
|
||||
<a-link class="copy-btn" @click="viewAuditLog(detailLog.request_id, detailLog.task_id || undefined)">查看审计日志</a-link>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="detailLog.task_id" class="detail-item">
|
||||
<div v-if="detailLog.task_id" class="detail-item detail-item-full">
|
||||
<span class="detail-label">关联任务</span>
|
||||
<span class="detail-value mono-text">
|
||||
{{ detailLog.task_id }}
|
||||
@@ -748,9 +847,9 @@ const { exporting, exportFile } = useExport({
|
||||
<span v-if="detailLog.channel_type" class="sub-text">({{ detailLog.channel_type }})</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div v-if="detailLog.relay_mode" class="detail-item">
|
||||
<span class="detail-label">代理模式</span>
|
||||
<span class="detail-value mono-text">{{ detailLog.relay_mode || '-' }}</span>
|
||||
<span class="detail-value mono-text">{{ detailLog.relay_mode }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">请求类型</span>
|
||||
@@ -769,18 +868,11 @@ const { exporting, exportFile } = useExport({
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">状态</span>
|
||||
<span class="detail-value">
|
||||
<a-tag :color="statusTagColor[detailLog.status]" size="small">
|
||||
{{ statusLabel[detailLog.status] || detailLog.status }}
|
||||
</a-tag>
|
||||
<span v-if="detailLog.retry_index > 0" class="retry-badge">重试 {{ detailLog.retry_index }} 次</span>
|
||||
<span class="detail-label">API Key</span>
|
||||
<span class="detail-value mono-text">
|
||||
{{ detailLog.api_key_name || detailLog.api_key_id || '-' }}<span v-if="detailLog.api_key_name && detailLog.api_key_id" class="sub-text">({{ detailLog.api_key_id }})</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">API Key ID</span>
|
||||
<span class="detail-value mono-text">{{ detailLog.api_key_id || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">客户端 IP</span>
|
||||
<span class="detail-value mono-text">{{ detailLog.client_ip || '-' }}</span>
|
||||
@@ -799,228 +891,69 @@ const { exporting, exportFile } = useExport({
|
||||
</div>
|
||||
<div v-if="detailLog.user_agent" class="detail-item detail-item-full">
|
||||
<span class="detail-label">User-Agent</span>
|
||||
<span class="detail-value sub-text" :title="detailLog.user_agent">{{ detailLog.user_agent }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">时间</span>
|
||||
<span class="detail-value">{{ formatTime(detailLog.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasUpstreamModel(detailLog)" class="detail-section">
|
||||
<div class="detail-section-title">
|
||||
<span class="detail-icon detail-icon-arrow"></span>
|
||||
模型映射
|
||||
</div>
|
||||
<div class="model-mapping">
|
||||
<a-tag>{{ detailLog.model_name }}</a-tag>
|
||||
<span class="mapping-arrow">→</span>
|
||||
<a-tag color="arcoblue">{{ detailLog.upstream_model }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">
|
||||
<span class="detail-icon detail-icon-chart"></span>
|
||||
Token 使用量
|
||||
</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><span class="token-dot token-dot-in"></span> 输入 Token</span>
|
||||
<span class="detail-value">{{ (detailLog.input_tokens || 0).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><span class="token-dot token-dot-out"></span> 输出 Token</span>
|
||||
<span class="detail-value">{{ (detailLog.output_tokens || 0).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><span class="token-dot token-dot-cache-read"></span> 缓存读取</span>
|
||||
<span class="detail-value token-cache-read">{{ (detailLog.cache_read_tokens || 0).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><span class="token-dot token-dot-cache-create"></span> 缓存创建</span>
|
||||
<span class="detail-value token-cache-create">{{ (detailLog.cache_creation_tokens || 0).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><span class="token-dot token-dot-cache-5m"></span> 缓存创建(5分钟)</span>
|
||||
<span class="detail-value token-cache-5m">{{ (detailLog.cache_creation_5m_tokens || 0).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><span class="token-dot token-dot-cache-1h"></span> 缓存创建(1小时)</span>
|
||||
<span class="detail-value token-cache-1h">{{ (detailLog.cache_creation_1h_tokens || 0).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="detailLog.reasoning_tokens > 0" class="detail-item">
|
||||
<span class="detail-label"><span class="token-dot token-dot-reasoning"></span> 推理 Token</span>
|
||||
<span class="detail-value token-reasoning">{{ detailLog.reasoning_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="detailLog.audio_input_tokens > 0" class="detail-item">
|
||||
<span class="detail-label"><span class="token-dot token-dot-audio"></span> 音频输入</span>
|
||||
<span class="detail-value token-audio">{{ detailLog.audio_input_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="detailLog.audio_output_tokens > 0" class="detail-item">
|
||||
<span class="detail-label"><span class="token-dot token-dot-audio"></span> 音频输出</span>
|
||||
<span class="detail-value token-audio">{{ detailLog.audio_output_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="detailLog.image_output_tokens > 0" class="detail-item">
|
||||
<span class="detail-label"><span class="token-dot token-dot-image"></span> 图像输出</span>
|
||||
<span class="detail-value token-image">{{ detailLog.image_output_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="detailLog.image_count > 0" class="detail-item">
|
||||
<span class="detail-label">生成图片</span>
|
||||
<span class="detail-value">
|
||||
{{ detailLog.image_count }} 张
|
||||
<span v-if="detailLog.image_size" class="sub-text">({{ detailLog.image_size }})</span>
|
||||
</span>
|
||||
<span class="detail-value sub-text detail-ellipsis" :title="detailLog.user_agent">{{ detailLog.user_agent }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">
|
||||
<span class="detail-icon detail-icon-credit"></span>
|
||||
费用明细
|
||||
<span class="detail-icon"><IconBarChart /></span>
|
||||
消费明细
|
||||
</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">输入费用</span>
|
||||
<span class="detail-value">{{ formatCost(detailLog.input_cost || 0) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">输出费用</span>
|
||||
<span class="detail-value">{{ formatCost(detailLog.output_cost || 0) }}</span>
|
||||
</div>
|
||||
<div v-if="detailLog.cache_creation_cost > 0" class="detail-item">
|
||||
<span class="detail-label">缓存创建费用</span>
|
||||
<span class="detail-value">{{ formatCost(detailLog.cache_creation_cost) }}</span>
|
||||
</div>
|
||||
<div v-if="detailLog.cache_read_cost > 0" class="detail-item">
|
||||
<span class="detail-label">缓存读取费用</span>
|
||||
<span class="detail-value">{{ formatCost(detailLog.cache_read_cost) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-summary">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">费率倍率</span>
|
||||
<span
|
||||
v-if="detailLog.rate_multiplier && detailLog.rate_multiplier !== 1"
|
||||
class="detail-value"
|
||||
:class="detailLog.rate_multiplier < 1 ? 'text-success' : 'text-warning'"
|
||||
>
|
||||
{{ detailLog.rate_multiplier.toFixed(4) }}x
|
||||
</span>
|
||||
<span v-else class="detail-value">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">基础费用</span>
|
||||
<span class="detail-value">{{ formatCost(detailLog.total_cost || 0) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">实际费用</span>
|
||||
<span class="detail-value text-success">{{ formatCost(detailLog.actual_cost || 0) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">定价来源</span>
|
||||
<span class="detail-value">{{ billingSourceLabel[detailLog.billing_source] || detailLog.billing_source || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">货币</span>
|
||||
<span class="detail-value">{{ detailLog.currency || 'USD' }}</span>
|
||||
<div class="receipt">
|
||||
<div class="receipt-head">
|
||||
<span class="receipt-brand">用量 · 费用 · 结算</span>
|
||||
<span class="receipt-meta">{{ detailLog.currency || 'USD' }}</span>
|
||||
</div>
|
||||
<!-- 小票行由 receiptRows 数据驱动生成 -->
|
||||
<template v-for="(row, index) in receiptRows" :key="index">
|
||||
<div v-if="row.divider" class="receipt-divider"></div>
|
||||
<div v-else class="receipt-row" :class="[row.strong ? 'receipt-strong' : '', row.total ? 'receipt-total' : '']">
|
||||
<span class="receipt-label">{{ row.label }}</span>
|
||||
<span class="receipt-value" :class="row.valueClass">
|
||||
{{ row.value }}<span v-if="row.sub" class="receipt-sub">{{ row.sub }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detailLog.pre_deduct_amount > 0 || detailLog.refund_amount > 0 || detailLog.supplement_amount > 0" class="detail-section">
|
||||
<div v-if="detailLog.billing_summary || snapshot" class="detail-section">
|
||||
<div class="detail-section-title">
|
||||
<span class="detail-icon detail-icon-refresh"></span>
|
||||
结算明细
|
||||
</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">预扣金额</span>
|
||||
<span class="detail-value">{{ formatCost(detailLog.pre_deduct_amount) }}</span>
|
||||
</div>
|
||||
<div v-if="detailLog.refund_amount > 0" class="detail-item">
|
||||
<span class="detail-label">退回金额</span>
|
||||
<span class="detail-value text-success">{{ formatCost(detailLog.refund_amount) }}</span>
|
||||
</div>
|
||||
<div v-if="detailLog.supplement_amount > 0" class="detail-item">
|
||||
<span class="detail-label">补扣金额</span>
|
||||
<span class="detail-value text-warning">{{ formatCost(detailLog.supplement_amount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">
|
||||
<span class="detail-icon detail-icon-clock"></span>
|
||||
性能指标
|
||||
</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">总延迟</span>
|
||||
<span class="detail-value">{{ formatMs(detailLog.latency_ms) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">首 Token 延迟</span>
|
||||
<span class="detail-value">{{ formatMs(detailLog.first_token_ms) }}</span>
|
||||
</div>
|
||||
<div v-if="detailLog.stream_end_reason" class="detail-item">
|
||||
<span class="detail-label">流结束原因</span>
|
||||
<span class="detail-value">
|
||||
<a-tag
|
||||
:color="detailLog.stream_end_reason === 'done' ? 'green' : 'gray'"
|
||||
size="medium"
|
||||
>
|
||||
{{ detailLog.stream_end_reason }}
|
||||
</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detailLog.billing_summary || parseSnapshot(detailLog)" class="detail-section">
|
||||
<div class="detail-section-title">
|
||||
<span class="detail-icon detail-icon-clipboard"></span>
|
||||
<span class="detail-icon"><IconCamera /></span>
|
||||
计费快照
|
||||
<a-link class="snapshot-toggle" @click="snapshotCollapsed = !snapshotCollapsed">{{ snapshotCollapsed ? '展开' : '收起' }}</a-link>
|
||||
</div>
|
||||
|
||||
<template v-if="parseSnapshot(detailLog)">
|
||||
<div v-if="snapshot && !snapshotCollapsed">
|
||||
<div class="snapshot-grid">
|
||||
<div class="snapshot-block">
|
||||
<div class="snapshot-block-title">定价信息</div>
|
||||
<div class="snapshot-block-body">
|
||||
<div v-if="parseSnapshot(detailLog).pricing.billing_source" class="snapshot-row">
|
||||
<span class="snapshot-label">价格来源</span>
|
||||
<span class="snapshot-value">{{ billingSourceLabel[parseSnapshot(detailLog).pricing.billing_source] || parseSnapshot(detailLog).pricing.billing_source }}</span>
|
||||
</div>
|
||||
<div v-if="parseSnapshot(detailLog).pricing.billing_mode" class="snapshot-row">
|
||||
<span class="snapshot-label">计费模式</span>
|
||||
<span class="snapshot-value">{{ billingModeLabel[parseSnapshot(detailLog).pricing.billing_mode] || parseSnapshot(detailLog).pricing.billing_mode }}</span>
|
||||
</div>
|
||||
<div class="snapshot-row">
|
||||
<span class="snapshot-label">基础输入价</span>
|
||||
<span class="snapshot-value">${{ (parseSnapshot(detailLog).pricing.base_input_price || 0).toFixed(6) }}/1M</span>
|
||||
<span class="snapshot-value">${{ (snapshot.pricing.base_input_price || 0).toFixed(6) }}/1M</span>
|
||||
</div>
|
||||
<div class="snapshot-row">
|
||||
<span class="snapshot-label">基础输出价</span>
|
||||
<span class="snapshot-value">${{ (parseSnapshot(detailLog).pricing.base_output_price || 0).toFixed(6) }}/1M</span>
|
||||
<span class="snapshot-value">${{ (snapshot.pricing.base_output_price || 0).toFixed(6) }}/1M</span>
|
||||
</div>
|
||||
<div v-if="parseSnapshot(detailLog).pricing.effective_input_price !== parseSnapshot(detailLog).pricing.base_input_price" class="snapshot-row">
|
||||
<div v-if="snapshot.pricing.effective_input_price !== snapshot.pricing.base_input_price" class="snapshot-row">
|
||||
<span class="snapshot-label">实际输入价</span>
|
||||
<span class="snapshot-value text-success">${{ (parseSnapshot(detailLog).pricing.effective_input_price || 0).toFixed(6) }}/1M</span>
|
||||
<span class="snapshot-value text-success">${{ (snapshot.pricing.effective_input_price || 0).toFixed(6) }}/1M</span>
|
||||
</div>
|
||||
<div v-if="parseSnapshot(detailLog).pricing.effective_output_price !== parseSnapshot(detailLog).pricing.base_output_price" class="snapshot-row">
|
||||
<div v-if="snapshot.pricing.effective_output_price !== snapshot.pricing.base_output_price" class="snapshot-row">
|
||||
<span class="snapshot-label">实际输出价</span>
|
||||
<span class="snapshot-value text-success">${{ (parseSnapshot(detailLog).pricing.effective_output_price || 0).toFixed(6) }}/1M</span>
|
||||
<span class="snapshot-value text-success">${{ (snapshot.pricing.effective_output_price || 0).toFixed(6) }}/1M</span>
|
||||
</div>
|
||||
<div v-if="parseSnapshot(detailLog).cache_prices && parseSnapshot(detailLog).cache_prices.cache_creation_price > 0" class="snapshot-row">
|
||||
<div v-if="snapshot.cache_prices && snapshot.cache_prices.cache_creation_price > 0" class="snapshot-row">
|
||||
<span class="snapshot-label">缓存创建单价</span>
|
||||
<span class="snapshot-value">${{ (parseSnapshot(detailLog).cache_prices.cache_creation_price || 0).toFixed(6) }}/1M</span>
|
||||
<span class="snapshot-value">${{ (snapshot.cache_prices.cache_creation_price || 0).toFixed(6) }}/1M</span>
|
||||
</div>
|
||||
<div v-if="parseSnapshot(detailLog).cache_prices && parseSnapshot(detailLog).cache_prices.cache_read_price > 0" class="snapshot-row">
|
||||
<div v-if="snapshot.cache_prices && snapshot.cache_prices.cache_read_price > 0" class="snapshot-row">
|
||||
<span class="snapshot-label">缓存读取单价</span>
|
||||
<span class="snapshot-value">${{ (parseSnapshot(detailLog).cache_prices.cache_read_price || 0).toFixed(6) }}/1M</span>
|
||||
<span class="snapshot-value">${{ (snapshot.cache_prices.cache_read_price || 0).toFixed(6) }}/1M</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1028,29 +961,29 @@ const { exporting, exportFile } = useExport({
|
||||
<div class="snapshot-block">
|
||||
<div class="snapshot-block-title">倍率信息</div>
|
||||
<div class="snapshot-block-body">
|
||||
<div v-if="parseSnapshot(detailLog).multipliers.model_multiplier && parseSnapshot(detailLog).multipliers.model_multiplier !== 1" class="snapshot-row">
|
||||
<div v-if="snapshot.multipliers.model_multiplier && snapshot.multipliers.model_multiplier !== 1" class="snapshot-row">
|
||||
<span class="snapshot-label">模型倍率</span>
|
||||
<span class="snapshot-value">{{ (parseSnapshot(detailLog).multipliers.model_multiplier).toFixed(4) }}x</span>
|
||||
<span class="snapshot-value">{{ (snapshot.multipliers.model_multiplier).toFixed(4) }}x</span>
|
||||
</div>
|
||||
<div class="snapshot-row">
|
||||
<span class="snapshot-label">租户倍率</span>
|
||||
<span class="snapshot-value" :class="(parseSnapshot(detailLog).multipliers.tenant_multiplier || 1) < 1 ? 'text-success' : ''">
|
||||
{{ (parseSnapshot(detailLog).multipliers.tenant_multiplier || 1).toFixed(4) }}x
|
||||
<span class="snapshot-value" :class="(snapshot.multipliers.tenant_multiplier || 1) < 1 ? 'text-success' : ''">
|
||||
{{ (snapshot.multipliers.tenant_multiplier || 1).toFixed(4) }}x
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="parseSnapshot(detailLog).multipliers.discount_ratio && parseSnapshot(detailLog).multipliers.discount_ratio !== 1" class="snapshot-row">
|
||||
<div v-if="snapshot.multipliers.discount_ratio && snapshot.multipliers.discount_ratio !== 1" class="snapshot-row">
|
||||
<span class="snapshot-label">折扣比例</span>
|
||||
<span class="snapshot-value text-success">{{ (parseSnapshot(detailLog).multipliers.discount_ratio).toFixed(4) }}x</span>
|
||||
<span class="snapshot-value text-success">{{ (snapshot.multipliers.discount_ratio).toFixed(4) }}x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="parseSnapshot(detailLog).token_costs" class="snapshot-block snapshot-block-full">
|
||||
<div v-if="snapshot.token_costs" class="snapshot-block snapshot-block-full">
|
||||
<div class="snapshot-block-title">Token 费用计算</div>
|
||||
<div class="snapshot-block-body">
|
||||
<template v-for="(tc, key) in parseSnapshot(detailLog).token_costs" :key="key">
|
||||
<template v-for="(tc, key) in snapshot.token_costs" :key="key">
|
||||
<div v-if="(tc.tokens || 0) > 0" class="snapshot-row">
|
||||
<span class="snapshot-label">{{ { input: '输入', output: '输出', cache_read: '缓存读取', cache_creation: '缓存创建', cache_creation_5m: '缓存创建(5分钟)', cache_creation_1h: '缓存创建(1小时)' }[key] || key }}</span>
|
||||
<span class="snapshot-label">{{ tokenCostLabels[key] || key }}</span>
|
||||
<span class="snapshot-value">
|
||||
{{ (tc.tokens || 0).toLocaleString() }} tokens × ${{ (tc.unit_price || 0).toFixed(6) }}/1M = <strong>${{ (tc.cost || 0).toFixed(6) }}</strong>
|
||||
</span>
|
||||
@@ -1059,76 +992,15 @@ const { exporting, exportFile } = useExport({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="parseSnapshot(detailLog).settlement" class="snapshot-block">
|
||||
<div class="snapshot-block-title">结算信息</div>
|
||||
<div class="snapshot-block-body">
|
||||
<div class="snapshot-row">
|
||||
<span class="snapshot-label">预扣金额</span>
|
||||
<span class="snapshot-value">{{ formatCost(parseSnapshot(detailLog).settlement.pre_deduct_amount || 0) }}</span>
|
||||
</div>
|
||||
<div class="snapshot-row">
|
||||
<span class="snapshot-label">实际费用</span>
|
||||
<span class="snapshot-value text-success">{{ formatCost(parseSnapshot(detailLog).settlement.actual_cost || 0) }}</span>
|
||||
</div>
|
||||
<div v-if="parseSnapshot(detailLog).settlement.refund_amount > 0" class="snapshot-row">
|
||||
<span class="snapshot-label">退还金额</span>
|
||||
<span class="snapshot-value text-success">{{ formatCost(parseSnapshot(detailLog).settlement.refund_amount) }}</span>
|
||||
</div>
|
||||
<div v-if="parseSnapshot(detailLog).settlement.supplement_amount > 0" class="snapshot-row">
|
||||
<span class="snapshot-label">补扣金额</span>
|
||||
<span class="snapshot-value text-warning">{{ formatCost(parseSnapshot(detailLog).settlement.supplement_amount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="parseSnapshot(detailLog).request_meta" class="snapshot-block">
|
||||
<div class="snapshot-block-title">请求元信息</div>
|
||||
<div class="snapshot-block-body">
|
||||
<div v-if="parseSnapshot(detailLog).request_meta.requested_model" class="snapshot-row">
|
||||
<span class="snapshot-label">请求模型</span>
|
||||
<span class="snapshot-value mono-text">{{ parseSnapshot(detailLog).request_meta.requested_model }}</span>
|
||||
</div>
|
||||
<div v-if="parseSnapshot(detailLog).request_meta.upstream_model" class="snapshot-row">
|
||||
<span class="snapshot-label">上游模型</span>
|
||||
<span class="snapshot-value mono-text">{{ parseSnapshot(detailLog).request_meta.upstream_model }}</span>
|
||||
</div>
|
||||
<div class="snapshot-row">
|
||||
<span class="snapshot-label">流式请求</span>
|
||||
<span class="snapshot-value">{{ parseSnapshot(detailLog).request_meta.is_stream ? '是' : '否' }}</span>
|
||||
</div>
|
||||
<div v-if="parseSnapshot(detailLog).request_meta.first_token_ms" class="snapshot-row">
|
||||
<span class="snapshot-label">首 Token</span>
|
||||
<span class="snapshot-value">{{ parseSnapshot(detailLog).request_meta.first_token_ms }}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="detailLog.billing_summary" style="margin-top: 12px;">
|
||||
<div v-if="detailLog.billing_summary && !snapshotCollapsed" style="margin-top: 12px;">
|
||||
<div class="snapshot-text-title">计算过程</div>
|
||||
<pre class="billing-snapshot">{{ detailLog.billing_summary }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detailLog.error_message" class="detail-section">
|
||||
<div class="detail-section-title error-title">
|
||||
<span class="detail-icon detail-icon-error"></span>
|
||||
错误信息
|
||||
</div>
|
||||
<div class="error-message">{{ detailLog.error_message }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">
|
||||
转发路径追踪
|
||||
</div>
|
||||
<a-spin :loading="forwardingTraceLoading" class="w-full">
|
||||
<ForwardingTracePanel v-if="forwardingTrace" :trace="forwardingTrace" />
|
||||
<a-empty v-else-if="!forwardingTraceLoading && !forwardingTraceFound" description="该请求未开启审计,无转发追踪数据" />
|
||||
<a-empty v-else-if="!forwardingTraceLoading && forwardingTraceFound" description="审计已记录,但未捕获转发追踪" />
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
@@ -1167,6 +1039,8 @@ const { exporting, exportFile } = useExport({
|
||||
.token-item > svg,
|
||||
.token-symbol {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.token-value {
|
||||
min-width: 0;
|
||||
@@ -1215,7 +1089,6 @@ const { exporting, exportFile } = useExport({
|
||||
}
|
||||
.cost-value {
|
||||
font-weight: 600;
|
||||
color: #00b42a;
|
||||
font-size: 13px;
|
||||
}
|
||||
.dark-tooltip {
|
||||
@@ -1300,9 +1173,6 @@ const { exporting, exportFile } = useExport({
|
||||
.detail-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.detail-section:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.detail-section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -1314,48 +1184,19 @@ const { exporting, exportFile } = useExport({
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.snapshot-toggle {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
.detail-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.detail-icon-doc {
|
||||
content: '';
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1.5px solid #165dff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.detail-icon-arrow {
|
||||
color: #165dff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.detail-icon-chart {
|
||||
color: #165dff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.detail-icon-credit {
|
||||
color: #165dff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.detail-icon-refresh {
|
||||
color: #165dff;
|
||||
font-size: 13px;
|
||||
}
|
||||
.detail-icon-clock {
|
||||
color: #165dff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.detail-icon-clipboard {
|
||||
color: #165dff;
|
||||
font-size: 13px;
|
||||
}
|
||||
.detail-icon-error {
|
||||
color: #f53f3f;
|
||||
font-size: 14px;
|
||||
}
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
@@ -1385,25 +1226,135 @@ const { exporting, exportFile } = useExport({
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
.detail-summary {
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid #e5e6eb;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 24px;
|
||||
/* 消费明细小票风格 */
|
||||
.receipt {
|
||||
margin-top: 8px;
|
||||
padding: 12px 14px;
|
||||
background: #fafafa;
|
||||
border: 1px dashed #d9dce0;
|
||||
border-radius: 8px;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.model-mapping {
|
||||
.receipt-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px dashed #d9dce0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.receipt-brand {
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.receipt-meta {
|
||||
color: #86909c;
|
||||
font-size: 11px;
|
||||
}
|
||||
.receipt-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
padding: 3.5px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.receipt-label {
|
||||
color: #86909c;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.receipt-value {
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.receipt-divider {
|
||||
margin: 6px 0;
|
||||
border-top: 1px dashed #d9dce0;
|
||||
}
|
||||
.receipt-total {
|
||||
margin-top: 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.receipt-total .receipt-label {
|
||||
color: #1d2129;
|
||||
font-weight: 600;
|
||||
}
|
||||
.receipt-total .receipt-value {
|
||||
font-size: 15px;
|
||||
}
|
||||
/* 小票小计行(Token 合计) */
|
||||
.receipt-strong .receipt-label {
|
||||
color: #1d2129;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* 小票值的灰色补充说明(如图片尺寸) */
|
||||
.receipt-sub {
|
||||
margin-left: 4px;
|
||||
color: #86909c;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
}
|
||||
/* 关键结果摘要条 */
|
||||
.detail-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px 14px;
|
||||
background: #f7f8fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.detail-summary-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #f7f8fa;
|
||||
border-radius: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.mapping-arrow {
|
||||
color: #86909c;
|
||||
.summary-model {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.summary-upstream {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #86909c;
|
||||
}
|
||||
.summary-cost {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1d2129;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.detail-summary-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
font-size: 12px;
|
||||
color: #4e5969;
|
||||
}
|
||||
.summary-dot {
|
||||
color: #c9cdd4;
|
||||
}
|
||||
.detail-summary-main .retry-badge {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* 超长值单行省略(配合 title 提示) */
|
||||
.detail-ellipsis {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mono-text {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
@@ -1475,10 +1426,8 @@ const { exporting, exportFile } = useExport({
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.error-title {
|
||||
.detail-section-title.error-title {
|
||||
border-left-color: #f53f3f;
|
||||
}
|
||||
.error-title .detail-section-title {
|
||||
color: #f53f3f;
|
||||
}
|
||||
.error-message {
|
||||
|
||||
Reference in new issue
Block a user