Commit ace0869dacdf8c3ed27bee2f33bf68d22972a1a8

Authored by yibo
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);
... ...