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,54 +28,62 @@
28 </div> 28 </div>
29 </section> 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 </div> 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 </div> 63 </div>
53 - <div class="trend-label">{{ t.label }}</div> 64 + <div v-if="timeStats.length === 0" class="empty-state">无数据</div>
54 </div> 65 </div>
55 - <div v-if="timeStats.length === 0" class="empty-state">无数据</div>  
56 </div> 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 </div> 77 </div>
67 - <div class="trend-label">{{ t.label }}</div> 78 + <div v-if="timeStats.length === 0" class="empty-state">无数据</div>
68 </div> 79 </div>
69 - <div v-if="timeStats.length === 0" class="empty-state">无数据</div>  
70 </div> 80 </div>
71 </div> 81 </div>
72 - </div>  
73 - </section> 82 + </section>
74 83
75 - <div class="charts-row">  
76 <!-- 按照 Agent 的消耗排行榜 --> 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 <div class="bar-chart"> 87 <div class="bar-chart">
80 <div v-for="agent in agentStats" :key="agent.id" class="bar-row"> 88 <div v-for="agent in agentStats" :key="agent.id" class="bar-row">
81 <div class="bar-label"> 89 <div class="bar-label">
@@ -95,100 +103,100 @@ @@ -95,100 +103,100 @@
95 <div v-if="agentStats.length === 0" class="empty-state">暂无数据</div> 103 <div v-if="agentStats.length === 0" class="empty-state">暂无数据</div>
96 </div> 104 </div>
97 </section> 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 </div> 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 </tr> 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 </main> 200 </main>
193 201
194 <div v-else class="loading-state"> 202 <div v-else class="loading-state">
@@ -207,6 +215,7 @@ const rawData = ref([]) @@ -207,6 +215,7 @@ const rawData = ref([])
207 215
208 const timeScale = ref('day') // hour, day, week, month 216 const timeScale = ref('day') // hour, day, week, month
209 const taskSort = ref('cost') // cost, tokens, time 217 const taskSort = ref('cost') // cost, tokens, time
  218 +const activeTab = ref('task') // 'trend', 'agent', 'task', 'log'
210 219
211 onMounted(async () => { 220 onMounted(async () => {
212 try { 221 try {
@@ -392,11 +401,36 @@ function formatTime(iso) { @@ -392,11 +401,36 @@ function formatTime(iso) {
392 cursor: help; 401 cursor: help;
393 } 402 }
394 403
395 -.charts-row { 404 +/* 导航 Tabs */
  405 +.tabs-nav {
396 display: flex; 406 display: flex;
397 - gap: 24px; 407 + gap: 12px;
398 margin-bottom: 24px; 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 .chart-card { 434 .chart-card {
401 background: var(--bg-surface); 435 background: var(--bg-surface);
402 border: 1px solid var(--border-color); 436 border: 1px solid var(--border-color);