feat: redesign token usage dashboard

This commit is contained in:
JOJO 2025-11-24 00:59:43 +08:00
parent 8c8b2d3a20
commit fd797e3e36
2 changed files with 217 additions and 82 deletions

View File

@ -333,99 +333,93 @@
<div class="token-drawer" v-if="currentConversationId" :class="{ collapsed: tokenPanelCollapsed }"> <div class="token-drawer" v-if="currentConversationId" :class="{ collapsed: tokenPanelCollapsed }">
<div class="token-display-panel"> <div class="token-display-panel">
<div class="token-panel-content"> <div class="token-panel-content">
<div class="token-panel-grid"> <div class="usage-dashboard">
<div class="panel-row"> <div class="usage-cell usage-cell--left usage-cell--token panel-card">
<div class="token-card compact panel-card"> <div class="usage-title">Token 统计</div>
<div class="panel-row-title">Token 统计</div> <div class="stat-grid stat-grid--triple">
<div class="token-stats inline-row"> <div class="stat-block">
<div class="token-item"> <div class="stat-label">当前上下文</div>
<span class="token-label">当前上下文</span> <div class="stat-value stat-value--accent">{{ formatTokenCount(currentContextTokens || 0) }}</div>
<span class="token-value current">{{ formatTokenCount(currentContextTokens || 0) }}</span>
</div>
<div class="token-item">
<span class="token-label">累计输入</span>
<span class="token-value input">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</span>
</div>
<div class="token-item">
<span class="token-label">累计输出</span>
<span class="token-value output">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</span>
</div>
</div> </div>
</div> <div class="stat-block">
<div class="container-card compact panel-card" v-if="containerStatus"> <div class="stat-label">累计输入</div>
<div class="panel-row-title"> <div class="stat-value stat-value--success">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</div>
<span class="token-label">CPU / 内存</span>
<span class="status-pill" :class="containerStatusClass()">{{ containerStatusText() }}</span>
</div> </div>
<div class="double-metric"> <div class="stat-block">
<div class="container-metric"> <div class="stat-label">累计输出</div>
<span class="metric-label">CPU</span> <div class="stat-value stat-value--warning">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</div>
<span class="metric-value">{{ formatPercentage(containerStatus.stats?.cpu_percent) }}</span>
</div>
<div class="container-metric">
<span class="metric-label">内存</span>
<span class="metric-value">
{{ formatBytes(containerStatus.stats?.memory?.used_bytes) }}
<template v-if="containerStatus.stats?.memory?.limit_bytes">
/ {{ formatBytes(containerStatus.stats.memory.limit_bytes) }}
</template>
</span>
<span class="metric-subtext" v-if="containerStatus.stats?.memory?.percent">
{{ formatPercentage(containerStatus.stats.memory.percent) }}
</span>
</div>
</div>
</div>
<div class="container-card compact panel-card empty" v-else>
<div class="container-empty">
当前运行在宿主机模式,暂无容器指标。
</div> </div>
</div> </div>
</div> </div>
<div class="panel-row"> <div class="usage-cell usage-cell--right usage-cell--performance panel-card">
<div class="token-card compact panel-card"> <div class="usage-title">
<div class="panel-row-title">模型 / 搜索 用量</div> <span>性能统计</span>
<div class="quota-inline"> <span class="status-pill" v-if="containerStatus" :class="containerStatusClass()">{{ containerStatusText() }}</span>
<div class="quota-inline-item"> </div>
<div class="quota-label">常规模型</div> <template v-if="hasContainerStats()">
<div class="quota-value">{{ formatQuotaValue(usageQuota.fast) }}</div> <div class="stat-grid stat-grid--double">
<div class="quota-reset" v-if="(usageQuota.fast.count || 0) > 0">重置 {{ formatResetTime(usageQuota.fast.reset_at) }}</div> <div class="stat-block">
<div class="stat-label">CPU</div>
<div class="stat-value">{{ formatPercentage(containerStatus.stats.cpu_percent) }}</div>
</div> </div>
<div class="quota-inline-item"> <div class="stat-block">
<div class="quota-label">思考模型</div> <div class="stat-label">内存</div>
<div class="quota-value">{{ formatQuotaValue(usageQuota.thinking) }}</div> <div class="stat-value stat-value--mono">
<div class="quota-reset" v-if="(usageQuota.thinking.count || 0) > 0">重置 {{ formatResetTime(usageQuota.thinking.reset_at) }}</div> {{ formatBytes(containerStatus.stats.memory.used_bytes) }}
<template v-if="containerStatus.stats.memory.limit_bytes">
/ {{ formatBytes(containerStatus.stats.memory.limit_bytes) }}
</template>
</div>
<div class="stat-foot" v-if="containerStatus.stats.memory.percent">
{{ formatPercentage(containerStatus.stats.memory.percent) }}
</div> </div>
<div class="quota-inline-item">
<div class="quota-label">搜索</div>
<div class="quota-value">{{ formatQuotaValue(usageQuota.search) }}</div>
<div class="quota-reset" v-if="(usageQuota.search.count || 0) > 0">重置 {{ formatResetTime(usageQuota.search.reset_at) }}</div>
</div> </div>
</div> </div>
</template>
<div class="usage-placeholder" v-else>
当前运行在宿主机模式,暂无容器指标。
</div>
</div>
<div class="usage-cell usage-cell--left usage-cell--quota panel-card">
<div class="usage-title">额度统计</div>
<div class="stat-grid stat-grid--triple">
<div class="stat-block">
<div class="stat-label">常规模型</div>
<div class="stat-value">{{ formatQuotaValue(usageQuota.fast) }}</div>
<div class="stat-foot" v-if="(usageQuota.fast.count || 0) > 0">重置 {{ formatResetTime(usageQuota.fast.reset_at) }}</div>
</div>
<div class="stat-block">
<div class="stat-label">思考模型</div>
<div class="stat-value">{{ formatQuotaValue(usageQuota.thinking) }}</div>
<div class="stat-foot" v-if="(usageQuota.thinking.count || 0) > 0">重置 {{ formatResetTime(usageQuota.thinking.reset_at) }}</div>
</div>
<div class="stat-block">
<div class="stat-label">搜索</div>
<div class="stat-value">{{ formatQuotaValue(usageQuota.search) }}</div>
<div class="stat-foot" v-if="(usageQuota.search.count || 0) > 0">重置 {{ formatResetTime(usageQuota.search.reset_at) }}</div>
</div>
</div> </div>
<div class="container-card compact panel-card"> </div>
<div class="panel-row-title"> <div class="usage-cell usage-cell--right usage-cell--resource panel-card">
<span class="token-label">网络 / 存储</span> <div class="usage-title">资源统计</div>
</div> <div class="stat-grid stat-grid--double">
<div class="double-metric"> <div class="stat-block">
<div class="container-metric"> <div class="stat-label">网络</div>
<span class="metric-label">网络</span> <div class="stat-value stat-value--mono">
<span class="metric-value"> ↓{{ formatRate(containerNetRate.down_bps) }}
↓{{ formatRate(containerNetRate.down_bps) }} ↑{{ formatRate(containerNetRate.up_bps) }}
↑{{ formatRate(containerNetRate.up_bps) }}
</span>
</div> </div>
<div class="container-metric"> </div>
<span class="metric-label">存储</span> <div class="stat-block">
<span class="metric-value"> <div class="stat-label">存储</div>
{{ formatBytes(projectStorage.used_bytes) }} <div class="stat-value stat-value--mono">
<template v-if="projectStorage.limit_bytes"> {{ formatBytes(projectStorage.used_bytes) }}
/ {{ formatBytes(projectStorage.limit_bytes) }} <template v-if="projectStorage.limit_bytes">
</template> / {{ formatBytes(projectStorage.limit_bytes) }}
</span> </template>
<span class="metric-subtext" v-if="typeof projectStorage.usage_percent === 'number'"> </div>
{{ projectStorage.usage_percent.toFixed(1) }}% <div class="stat-foot" v-if="typeof projectStorage.usage_percent === 'number'">
</span> {{ projectStorage.usage_percent.toFixed(1) }}%
</div> </div>
</div> </div>
</div> </div>

View File

@ -2430,6 +2430,128 @@ o-files {
gap: 24px; gap: 24px;
} }
.usage-dashboard {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: auto;
column-gap: 48px;
row-gap: 32px;
padding: 8px 0;
}
.usage-dashboard::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 1px;
background: var(--claude-border);
transform: translateX(-0.5px);
pointer-events: none;
z-index: 0;
}
.usage-cell {
display: flex;
flex-direction: column;
gap: 16px;
position: relative;
z-index: 1;
}
.usage-dashboard .panel-card {
border-left: none;
padding: 0;
}
.usage-cell--left {
padding-right: 24px;
}
.usage-cell--right {
padding-left: 24px;
}
.usage-title {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
font-weight: 600;
color: var(--claude-text);
}
.stat-grid {
display: grid;
gap: 12px;
}
.stat-grid--triple {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.stat-grid--double {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.stat-block {
display: flex;
flex-direction: column;
gap: 6px;
}
.stat-label {
color: var(--claude-text-secondary);
font-size: 12px;
letter-spacing: 0.04em;
}
.stat-value {
font-weight: 600;
font-size: 18px;
font-variant-numeric: tabular-nums;
color: var(--claude-text);
}
.stat-value--accent {
color: var(--claude-accent);
font-size: 20px;
}
.stat-value--success {
color: var(--claude-success);
}
.stat-value--warning {
color: var(--claude-warning);
}
.stat-value--mono {
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
font-size: 16px;
}
.stat-meta,
.stat-foot {
font-size: 12px;
color: var(--claude-text-secondary);
}
.stat-meta {
text-transform: none;
}
.stat-foot {
margin-top: 2px;
}
.usage-placeholder {
font-size: 13px;
color: var(--claude-text-secondary);
}
.panel-row { .panel-row {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
@ -2662,6 +2784,25 @@ o-files {
gap: 16px; gap: 16px;
} }
.usage-dashboard {
grid-template-columns: 1fr;
row-gap: 20px;
padding: 0;
}
.usage-dashboard::before {
display: none;
}
.usage-cell--left,
.usage-cell--right {
padding: 0;
}
.stat-grid--triple {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.token-item { .token-item {
min-width: auto; min-width: auto;
} }