Commit ace0869dacdf8c3ed27bee2f33bf68d22972a1a8
1 parent
958af56b
style: refactor quota monitor into a 4-tab layout
Converted the Quota Monitor's vertical scroll layout into a clean 4-tab interface: 1. Trend Statistics (使用趋势统计) 2. Agent Ranking (各 Agent 计算力消耗排行榜) 3. Task Breakdown (任务级成本分析) 4. Recent Logs (最近的大模型调用日志) This significantly improves usability and avoids excessive scrolling while keeping the top overview indicators universally visible.
Showing
1 changed file
with
163 additions
and
129 deletions
web/src/views/QuotaMonitor.vue
| ... | ... | @@ -28,54 +28,62 @@ |
| 28 | 28 | </div> |
| 29 | 29 | </section> |
| 30 | 30 | |
| 31 | - <!-- 按照时间维度的调用频次柱状图 --> | |
| 32 | - <section class="chart-card flex-double time-chart-section"> | |
| 33 | - <div class="card-header"> | |
| 34 | - <h3>📈 使用趋势统计</h3> | |
| 35 | - <div class="time-filters"> | |
| 36 | - <button :class="{active: timeScale === 'hour'}" @click="timeScale = 'hour'">按小时</button> | |
| 37 | - <button :class="{active: timeScale === 'day'}" @click="timeScale = 'day'">按天</button> | |
| 38 | - <button :class="{active: timeScale === 'week'}" @click="timeScale = 'week'">按周</button> | |
| 39 | - <button :class="{active: timeScale === 'month'}" @click="timeScale = 'month'">按月</button> | |
| 31 | + <!-- 导航 Tabs --> | |
| 32 | + <div class="tabs-nav"> | |
| 33 | + <button :class="{active: activeTab === 'trend'}" @click="activeTab = 'trend'">📈 使用趋势统计</button> | |
| 34 | + <button :class="{active: activeTab === 'agent'}" @click="activeTab = 'agent'">🤖 Agent 排行榜</button> | |
| 35 | + <button :class="{active: activeTab === 'task'}" @click="activeTab = 'task'">🎯 任务级成本分析</button> | |
| 36 | + <button :class="{active: activeTab === 'log'}" @click="activeTab = 'log'">🕒 最近调用日志</button> | |
| 37 | + </div> | |
| 38 | + | |
| 39 | + <div class="tab-content"> | |
| 40 | + <!-- 按照时间维度的调用频次柱状图 --> | |
| 41 | + <section v-if="activeTab === 'trend'" class="chart-card flex-double time-chart-section"> | |
| 42 | + <div class="card-header"> | |
| 43 | + <h3>使用趋势统计</h3> | |
| 44 | + <div class="time-filters"> | |
| 45 | + <button :class="{active: timeScale === 'hour'}" @click="timeScale = 'hour'">按小时</button> | |
| 46 | + <button :class="{active: timeScale === 'day'}" @click="timeScale = 'day'">按天</button> | |
| 47 | + <button :class="{active: timeScale === 'week'}" @click="timeScale = 'week'">按周</button> | |
| 48 | + <button :class="{active: timeScale === 'month'}" @click="timeScale = 'month'">按月</button> | |
| 49 | + </div> | |
| 40 | 50 | </div> |
| 41 | - </div> | |
| 42 | - | |
| 43 | - <div class="trend-charts-wrapper"> | |
| 44 | - <!-- Tokens 柱状图 --> | |
| 45 | - <div class="chart-box"> | |
| 46 | - <h4 class="chart-title">Token 消耗量 <span class="legend"><span class="c-in">■</span>输入 <span class="c-out">■</span>输出</span></h4> | |
| 47 | - <div class="trend-bars"> | |
| 48 | - <div v-for="t in timeStats" :key="t.label" class="trend-bar-col"> | |
| 49 | - <div class="bar-stack" title="Input: {{ t.input }} | Output: {{ t.output }}"> | |
| 50 | - <div class="bar-portion-out" :style="{ height: ((t.output / maxTimeTokens) * 100) + '%' }"></div> | |
| 51 | - <div class="bar-portion-in" :style="{ height: ((t.input / maxTimeTokens) * 100) + '%' }"></div> | |
| 51 | + | |
| 52 | + <div class="trend-charts-wrapper"> | |
| 53 | + <!-- Tokens 柱状图 --> | |
| 54 | + <div class="chart-box"> | |
| 55 | + <h4 class="chart-title">Token 消耗量 <span class="legend"><span class="c-in">■</span>输入 <span class="c-out">■</span>输出</span></h4> | |
| 56 | + <div class="trend-bars"> | |
| 57 | + <div v-for="t in timeStats" :key="t.label" class="trend-bar-col"> | |
| 58 | + <div class="bar-stack" title="Input: {{ t.input }} | Output: {{ t.output }}"> | |
| 59 | + <div class="bar-portion-out" :style="{ height: ((t.output / maxTimeTokens) * 100) + '%' }"></div> | |
| 60 | + <div class="bar-portion-in" :style="{ height: ((t.input / maxTimeTokens) * 100) + '%' }"></div> | |
| 61 | + </div> | |
| 62 | + <div class="trend-label">{{ t.label }}</div> | |
| 52 | 63 | </div> |
| 53 | - <div class="trend-label">{{ t.label }}</div> | |
| 64 | + <div v-if="timeStats.length === 0" class="empty-state">无数据</div> | |
| 54 | 65 | </div> |
| 55 | - <div v-if="timeStats.length === 0" class="empty-state">无数据</div> | |
| 56 | 66 | </div> |
| 57 | - </div> | |
| 58 | 67 | |
| 59 | - <!-- 调用次数柱状图 --> | |
| 60 | - <div class="chart-box"> | |
| 61 | - <h4 class="chart-title">API 调用频次</h4> | |
| 62 | - <div class="trend-bars"> | |
| 63 | - <div v-for="t in timeStats" :key="t.label" class="trend-bar-col"> | |
| 64 | - <div class="bar-stack" title="{{ t.calls }} 次"> | |
| 65 | - <div class="bar-portion-calls" :style="{ height: ((t.calls / maxTimeCalls) * 100) + '%' }"></div> | |
| 68 | + <!-- 调用次数柱状图 --> | |
| 69 | + <div class="chart-box"> | |
| 70 | + <h4 class="chart-title">API 调用频次</h4> | |
| 71 | + <div class="trend-bars"> | |
| 72 | + <div v-for="t in timeStats" :key="t.label" class="trend-bar-col"> | |
| 73 | + <div class="bar-stack" title="{{ t.calls }} 次"> | |
| 74 | + <div class="bar-portion-calls" :style="{ height: ((t.calls / maxTimeCalls) * 100) + '%' }"></div> | |
| 75 | + </div> | |
| 76 | + <div class="trend-label">{{ t.label }}</div> | |
| 66 | 77 | </div> |
| 67 | - <div class="trend-label">{{ t.label }}</div> | |
| 78 | + <div v-if="timeStats.length === 0" class="empty-state">无数据</div> | |
| 68 | 79 | </div> |
| 69 | - <div v-if="timeStats.length === 0" class="empty-state">无数据</div> | |
| 70 | 80 | </div> |
| 71 | 81 | </div> |
| 72 | - </div> | |
| 73 | - </section> | |
| 82 | + </section> | |
| 74 | 83 | |
| 75 | - <div class="charts-row"> | |
| 76 | 84 | <!-- 按照 Agent 的消耗排行榜 --> |
| 77 | - <section class="chart-card flex-double"> | |
| 78 | - <h3>🤖 各 Agent 计算力消耗排行榜</h3> | |
| 85 | + <section v-if="activeTab === 'agent'" class="chart-card flex-double"> | |
| 86 | + <h3>各 Agent 计算力消耗排行榜</h3> | |
| 79 | 87 | <div class="bar-chart"> |
| 80 | 88 | <div v-for="agent in agentStats" :key="agent.id" class="bar-row"> |
| 81 | 89 | <div class="bar-label"> |
| ... | ... | @@ -95,100 +103,100 @@ |
| 95 | 103 | <div v-if="agentStats.length === 0" class="empty-state">暂无数据</div> |
| 96 | 104 | </div> |
| 97 | 105 | </section> |
| 98 | - </div> | |
| 99 | 106 | |
| 100 | - <!-- 🆕 按任务链聚合的成本分析 --> | |
| 101 | - <section class="chart-card"> | |
| 102 | - <div class="card-header"> | |
| 103 | - <h3>🎯 任务级成本分析</h3> | |
| 104 | - <div class="time-filters"> | |
| 105 | - <button :class="{active: taskSort === 'cost'}" @click="taskSort = 'cost'">按花费</button> | |
| 106 | - <button :class="{active: taskSort === 'tokens'}" @click="taskSort = 'tokens'">按 Tokens</button> | |
| 107 | - <button :class="{active: taskSort === 'time'}" @click="taskSort = 'time'">按时间</button> | |
| 107 | + <!-- 🆕 按任务链聚合的成本分析 --> | |
| 108 | + <section v-if="activeTab === 'task'" class="chart-card"> | |
| 109 | + <div class="card-header"> | |
| 110 | + <h3>任务级成本分析</h3> | |
| 111 | + <div class="time-filters"> | |
| 112 | + <button :class="{active: taskSort === 'cost'}" @click="taskSort = 'cost'">按花费</button> | |
| 113 | + <button :class="{active: taskSort === 'tokens'}" @click="taskSort = 'tokens'">按 Tokens</button> | |
| 114 | + <button :class="{active: taskSort === 'time'}" @click="taskSort = 'time'">按时间</button> | |
| 115 | + </div> | |
| 108 | 116 | </div> |
| 109 | - </div> | |
| 110 | - <div class="task-table-wrapper"> | |
| 111 | - <table class="log-table task-table"> | |
| 112 | - <thead> | |
| 113 | - <tr> | |
| 114 | - <th>任务 ID</th> | |
| 115 | - <th>Agent 链路</th> | |
| 116 | - <th>调用次数</th> | |
| 117 | - <th>Input Tokens</th> | |
| 118 | - <th>Output Tokens</th> | |
| 119 | - <th>总耗时</th> | |
| 120 | - <th>总花费</th> | |
| 121 | - </tr> | |
| 122 | - </thead> | |
| 123 | - <tbody> | |
| 124 | - <tr v-for="task in taskStats" :key="task.taskId" | |
| 125 | - :class="{'task-row-expensive': task.cost > 2}" | |
| 126 | - @click="task.expanded = !task.expanded"> | |
| 127 | - <td class="log-agent task-id-cell"> | |
| 128 | - <span class="expand-icon">{{ task.expanded ? '▼' : '▶' }}</span> | |
| 129 | - {{ task.taskId }} | |
| 130 | - </td> | |
| 131 | - <td class="task-agents"> | |
| 132 | - <span v-for="(a, i) in task.agents" :key="a" class="agent-badge"> | |
| 133 | - {{ a }}<span v-if="i < task.agents.length - 1"> → </span> | |
| 134 | - </span> | |
| 135 | - </td> | |
| 136 | - <td>{{ task.calls }}</td> | |
| 137 | - <td class="log-tokens in">{{ (task.input / 1000).toFixed(0) }}k</td> | |
| 138 | - <td class="log-tokens out">{{ (task.output / 1000).toFixed(0) }}k</td> | |
| 139 | - <td class="log-duration">{{ (task.duration / 1000).toFixed(0) }}s</td> | |
| 140 | - <td class="log-cost" :class="{'cost-warn': task.cost > 2, 'cost-danger': task.cost > 5}"> | |
| 141 | - ${{ task.cost.toFixed(2) }} | |
| 142 | - </td> | |
| 143 | - </tr> | |
| 144 | - <template v-for="task in taskStats" :key="'detail-'+task.taskId"> | |
| 145 | - <tr v-if="task.expanded" v-for="(call, ci) in task.details" :key="ci" class="task-detail-row"> | |
| 146 | - <td></td> | |
| 147 | - <td class="log-agent">{{ call.agentId }}</td> | |
| 148 | - <td class="log-model">{{ call.convId }}</td> | |
| 149 | - <td class="log-tokens in">{{ (call.usage?.input || 0).toLocaleString() }}</td> | |
| 150 | - <td class="log-tokens out">{{ (call.usage?.output || 0).toLocaleString() }}</td> | |
| 151 | - <td class="log-duration">{{ (call.durationMs / 1000).toFixed(1) }}s</td> | |
| 152 | - <td class="log-cost">${{ call.costUsd.toFixed(4) }}</td> | |
| 117 | + <div class="task-table-wrapper"> | |
| 118 | + <table class="log-table task-table"> | |
| 119 | + <thead> | |
| 120 | + <tr> | |
| 121 | + <th>任务 ID</th> | |
| 122 | + <th>Agent 链路</th> | |
| 123 | + <th>调用次数</th> | |
| 124 | + <th>Input Tokens</th> | |
| 125 | + <th>Output Tokens</th> | |
| 126 | + <th>总耗时</th> | |
| 127 | + <th>总花费</th> | |
| 153 | 128 | </tr> |
| 154 | - </template> | |
| 155 | - </tbody> | |
| 156 | - </table> | |
| 157 | - <div v-if="taskStats.length === 0" class="empty-state">暂无任务数据</div> | |
| 158 | - </div> | |
| 159 | - </section> | |
| 129 | + </thead> | |
| 130 | + <tbody> | |
| 131 | + <tr v-for="task in taskStats" :key="task.taskId" | |
| 132 | + :class="{'task-row-expensive': task.cost > 2}" | |
| 133 | + @click="task.expanded = !task.expanded"> | |
| 134 | + <td class="log-agent task-id-cell"> | |
| 135 | + <span class="expand-icon">{{ task.expanded ? '▼' : '▶' }}</span> | |
| 136 | + {{ task.taskId }} | |
| 137 | + </td> | |
| 138 | + <td class="task-agents"> | |
| 139 | + <span v-for="(a, i) in task.agents" :key="a" class="agent-badge"> | |
| 140 | + {{ a }}<span v-if="i < task.agents.length - 1"> → </span> | |
| 141 | + </span> | |
| 142 | + </td> | |
| 143 | + <td>{{ task.calls }}</td> | |
| 144 | + <td class="log-tokens in">{{ (task.input / 1000).toFixed(0) }}k</td> | |
| 145 | + <td class="log-tokens out">{{ (task.output / 1000).toFixed(0) }}k</td> | |
| 146 | + <td class="log-duration">{{ (task.duration / 1000).toFixed(0) }}s</td> | |
| 147 | + <td class="log-cost" :class="{'cost-warn': task.cost > 2, 'cost-danger': task.cost > 5}"> | |
| 148 | + ${{ task.cost.toFixed(2) }} | |
| 149 | + </td> | |
| 150 | + </tr> | |
| 151 | + <template v-for="task in taskStats" :key="'detail-'+task.taskId"> | |
| 152 | + <tr v-if="task.expanded" v-for="(call, ci) in task.details" :key="ci" class="task-detail-row"> | |
| 153 | + <td></td> | |
| 154 | + <td class="log-agent">{{ call.agentId }}</td> | |
| 155 | + <td class="log-model">{{ call.convId }}</td> | |
| 156 | + <td class="log-tokens in">{{ (call.usage?.input || 0).toLocaleString() }}</td> | |
| 157 | + <td class="log-tokens out">{{ (call.usage?.output || 0).toLocaleString() }}</td> | |
| 158 | + <td class="log-duration">{{ (call.durationMs / 1000).toFixed(1) }}s</td> | |
| 159 | + <td class="log-cost">${{ call.costUsd.toFixed(4) }}</td> | |
| 160 | + </tr> | |
| 161 | + </template> | |
| 162 | + </tbody> | |
| 163 | + </table> | |
| 164 | + <div v-if="taskStats.length === 0" class="empty-state">暂无任务数据</div> | |
| 165 | + </div> | |
| 166 | + </section> | |
| 160 | 167 | |
| 161 | - <!-- 详细调用日志 --> | |
| 162 | - <section class="chart-card"> | |
| 163 | - <h3>🕒 最近的大模型调用日志</h3> | |
| 164 | - <div class="log-table-wrapper"> | |
| 165 | - <table class="log-table"> | |
| 166 | - <thead> | |
| 167 | - <tr> | |
| 168 | - <th>时间</th> | |
| 169 | - <th>模型 (Model)</th> | |
| 170 | - <th>Agent</th> | |
| 171 | - <th>耗时</th> | |
| 172 | - <th>输入 Tokens</th> | |
| 173 | - <th>输出 Tokens</th> | |
| 174 | - <th>计算预估*</th> | |
| 175 | - </tr> | |
| 176 | - </thead> | |
| 177 | - <tbody> | |
| 178 | - <tr v-for="(log, i) in recentLogs" :key="i"> | |
| 179 | - <td class="log-time">{{ formatTime(log.timestamp) }}</td> | |
| 180 | - <td class="log-model">{{ log.model || 'unknown' }}</td> | |
| 181 | - <td class="log-agent">{{ log.agentId }}</td> | |
| 182 | - <td class="log-duration">{{ (log.durationMs / 1000).toFixed(1) }}s</td> | |
| 183 | - <td class="log-tokens in">+{{ (log.usage?.input || 0).toLocaleString() }}</td> | |
| 184 | - <td class="log-tokens out">+{{ (log.usage?.output || 0).toLocaleString() }}</td> | |
| 185 | - <td class="log-cost">${{ log.costUsd.toFixed(6) }}</td> | |
| 186 | - </tr> | |
| 187 | - </tbody> | |
| 188 | - </table> | |
| 189 | - <div v-if="recentLogs.length === 0" class="empty-state">暂无日志</div> | |
| 190 | - </div> | |
| 191 | - </section> | |
| 168 | + <!-- 详细调用日志 --> | |
| 169 | + <section v-if="activeTab === 'log'" class="chart-card"> | |
| 170 | + <h3>最近的大模型调用日志</h3> | |
| 171 | + <div class="log-table-wrapper"> | |
| 172 | + <table class="log-table"> | |
| 173 | + <thead> | |
| 174 | + <tr> | |
| 175 | + <th>时间</th> | |
| 176 | + <th>模型 (Model)</th> | |
| 177 | + <th>Agent</th> | |
| 178 | + <th>耗时</th> | |
| 179 | + <th>输入 Tokens</th> | |
| 180 | + <th>输出 Tokens</th> | |
| 181 | + <th>计算预估*</th> | |
| 182 | + </tr> | |
| 183 | + </thead> | |
| 184 | + <tbody> | |
| 185 | + <tr v-for="(log, i) in recentLogs" :key="i"> | |
| 186 | + <td class="log-time">{{ formatTime(log.timestamp) }}</td> | |
| 187 | + <td class="log-model">{{ log.model || 'unknown' }}</td> | |
| 188 | + <td class="log-agent">{{ log.agentId }}</td> | |
| 189 | + <td class="log-duration">{{ (log.durationMs / 1000).toFixed(1) }}s</td> | |
| 190 | + <td class="log-tokens in">+{{ (log.usage?.input || 0).toLocaleString() }}</td> | |
| 191 | + <td class="log-tokens out">+{{ (log.usage?.output || 0).toLocaleString() }}</td> | |
| 192 | + <td class="log-cost">${{ log.costUsd.toFixed(6) }}</td> | |
| 193 | + </tr> | |
| 194 | + </tbody> | |
| 195 | + </table> | |
| 196 | + <div v-if="recentLogs.length === 0" class="empty-state">暂无日志</div> | |
| 197 | + </div> | |
| 198 | + </section> | |
| 199 | + </div> | |
| 192 | 200 | </main> |
| 193 | 201 | |
| 194 | 202 | <div v-else class="loading-state"> |
| ... | ... | @@ -207,6 +215,7 @@ const rawData = ref([]) |
| 207 | 215 | |
| 208 | 216 | const timeScale = ref('day') // hour, day, week, month |
| 209 | 217 | const taskSort = ref('cost') // cost, tokens, time |
| 218 | +const activeTab = ref('task') // 'trend', 'agent', 'task', 'log' | |
| 210 | 219 | |
| 211 | 220 | onMounted(async () => { |
| 212 | 221 | try { |
| ... | ... | @@ -392,11 +401,36 @@ function formatTime(iso) { |
| 392 | 401 | cursor: help; |
| 393 | 402 | } |
| 394 | 403 | |
| 395 | -.charts-row { | |
| 404 | +/* 导航 Tabs */ | |
| 405 | +.tabs-nav { | |
| 396 | 406 | display: flex; |
| 397 | - gap: 24px; | |
| 407 | + gap: 12px; | |
| 398 | 408 | margin-bottom: 24px; |
| 409 | + border-bottom: 1px solid var(--border-color); | |
| 410 | + padding-bottom: 12px; | |
| 399 | 411 | } |
| 412 | +.tabs-nav button { | |
| 413 | + background: transparent; | |
| 414 | + border: 1px solid transparent; | |
| 415 | + font-size: 14px; | |
| 416 | + font-weight: 500; | |
| 417 | + padding: 8px 16px; | |
| 418 | + border-radius: 8px; | |
| 419 | + color: var(--text-secondary); | |
| 420 | + cursor: pointer; | |
| 421 | + transition: all 0.2s; | |
| 422 | +} | |
| 423 | +.tabs-nav button:hover { | |
| 424 | + background: rgba(255, 255, 255, 0.05); | |
| 425 | + color: var(--text-primary); | |
| 426 | +} | |
| 427 | +.tabs-nav button.active { | |
| 428 | + background: var(--bg-surface); | |
| 429 | + border-color: var(--border-color); | |
| 430 | + color: var(--accent-primary); | |
| 431 | + box-shadow: 0 2px 4px rgba(0,0,0,0.05); | |
| 432 | +} | |
| 433 | + | |
| 400 | 434 | .chart-card { |
| 401 | 435 | background: var(--bg-surface); |
| 402 | 436 | border: 1px solid var(--border-color); | ... | ... |