improve(ui):优化管理后台的表格显示

This commit is contained in:
chengqian committed 2026-08-17 17:37:22 +08:00
1 parent 6f96eb4150
commit 014e0f1736
2 files changed
+39 -34

No files matched your search

@@ -1,17 +1,4 @@
-- +goose Up
-- input_tokens 口径统一:bil_usage_logs.input_tokens 从此统一为「含缓存的总输入」。
-- 历史问题:OpenAI/Gemini 渠道(promptTokenCount/prompt_tokens 原生含缓存)与 Claude 渠道
-- (input_tokens 原生不含缓存)写入同一列但语义不同,跨渠道 SUM 聚合与缓存命中率分母失真。
-- 写侧已改为统一按 TotalInputTokens() 入库(Claude 口径补加 cache_read + cache_creation),
-- 本迁移回填历史 Claude 渠道行(channel_type=2)使存量数据同口径。
-- 上线后需用 aggregate-usage 命令回填 bil_usage_daily 历史区间(ON CONFLICT 幂等):
-- team-api aggregate-usage --from <最早用量日期> --to <今天>
UPDATE bil_usage_logs
SET input_tokens = input_tokens + COALESCE(cache_read_tokens, 0) + COALESCE(cache_creation_tokens, 0),
total_tokens = total_tokens + COALESCE(cache_read_tokens, 0) + COALESCE(cache_creation_tokens, 0)
WHERE channel_type = 2
AND (COALESCE(cache_read_tokens, 0) + COALESCE(cache_creation_tokens, 0)) > 0;
-- 列语义注释同步更新(cache_creation_tokens 兼收 OpenAI Responses 的 cache_write_tokens)
COMMENT ON COLUMN bil_usage_logs.input_tokens IS '输入 token 数(含缓存总输入:base + cache_read + cache_creation,跨渠道统一口径)';
@@ -20,11 +7,3 @@ COMMENT ON COLUMN bil_usage_daily.input_tokens IS '输入Token合计(含缓存
COMMENT ON COLUMN bil_usage_daily.cache_creation_tokens IS '写入缓存的 token 数(Claude cache_creation / OpenAI cache_write,SUM 聚合)';
-- +goose Down
-- 逆转回填:Claude 渠道行恢复「不含缓存」口径(OpenAI/Gemini 行本就含缓存,不受影响)。
-- 注意:Down 后需再次执行 aggregate-usage 回滚 bil_usage_daily。
UPDATE bil_usage_logs
SET input_tokens = input_tokens - COALESCE(cache_read_tokens, 0) - COALESCE(cache_creation_tokens, 0),
total_tokens = total_tokens - COALESCE(cache_read_tokens, 0) - COALESCE(cache_creation_tokens, 0)
WHERE channel_type = 2
AND (COALESCE(cache_read_tokens, 0) + COALESCE(cache_creation_tokens, 0)) > 0;
+39 -13
View File
@@ -4,6 +4,7 @@ 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 type { TableColumnData } from '@arco-design/web-vue'
import PageHeader from '@/components/PageHeader.vue'
import TableStats from '@/components/TableStats.vue'
@@ -278,7 +279,7 @@ const columns: TableColumnData[] = [
},
},
{
title: 'Token', dataIndex: 'tokens', width: 250,
title: 'Token', dataIndex: 'tokens', width: 180,
render({ record }) {
const tooltipContent = [
h('div', { class: 'dark-tooltip-title' }, 'Token 详情'),
@@ -305,19 +306,31 @@ const columns: TableColumnData[] = [
position: 'right',
}, {
default: () => h('div', { class: 'token-cell' }, [
h('div', { class: 'token-row' }, [
h('span', { class: 'token-item', style: 'color: #18a058' }, `↑${(record.input_tokens || 0).toLocaleString()}`),
h('span', { class: 'token-item', style: 'color: #722ed1' }, `↓${(record.output_tokens || 0).toLocaleString()}`),
h('span', { class: 'token-item', style: 'color: #ff7d00' }, `✎${(record.cache_creation_tokens || 0).toLocaleString()}`),
h('span', { class: 'token-item', style: 'color: #0fc6c2' }, `⚡${(record.cache_read_tokens || 0).toLocaleString()}`),
h('div', { class: 'token-row' }, [
h('span', { class: 'token-item', style: 'color: #18a058', 'aria-label': `输入 Token ${(record.input_tokens || 0).toLocaleString()}` }, [
h('span', { class: 'token-symbol', 'aria-hidden': 'true' }, '↑'),
h('span', { class: 'token-value' }, (record.input_tokens || 0).toLocaleString()),
]),
h('span', { class: 'token-item', style: 'color: #722ed1', 'aria-label': `输出 Token ${(record.output_tokens || 0).toLocaleString()}` }, [
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: #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),
})
},
},
{
title: '费用', dataIndex: 'cost', width: 120,
title: '费用', dataIndex: 'cost', width: 100,
render({ record }) {
const tooltipContent = [
h('div', { class: 'dark-tooltip-title' }, '费用明细'),
@@ -364,7 +377,7 @@ const columns: TableColumnData[] = [
},
},
{
title: '状态', dataIndex: 'status', width: 90,
title: '状态', dataIndex: 'status', width: 80,
render({ record }) {
const items: any[] = [
h(Tag, { color: statusTagColor[record.status], size: 'small' }, () => statusLabel[record.status] || record.status),
@@ -376,7 +389,7 @@ const columns: TableColumnData[] = [
},
},
{
title: '时间', dataIndex: 'created_at', width: 180,
title: '时间', dataIndex: 'created_at', width: 160,
render({ record }) {
return h('span', { class: 'time-text' }, formatTime(record.created_at))
},
@@ -1041,13 +1054,16 @@ const { exporting, exportFile } = useExport({
background: rgba(22, 93, 255, 0.06);
}
.token-row {
display: flex;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: center;
justify-content: flex-start;
line-height: 1.8;
column-gap: 12px;
row-gap: 0;
line-height: 1.4;
}
.token-item {
min-width: 50px;
min-width: 0;
width: 100%;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
@@ -1055,6 +1071,16 @@ const { exporting, exportFile } = useExport({
align-items: center;
gap: 4px;
}
.token-item > svg,
.token-symbol {
flex-shrink: 0;
}
.token-value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
font-variant-numeric: tabular-nums;
}
.token-in { color: #18a058; }
.token-out { color: #722ed1; }
.token-cache-read { color: #0fc6c2; }