mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
feat: add cost period selection and update cost chart functionality
This commit is contained in:
8
app.js
8
app.js
@@ -503,6 +503,8 @@ class CLIProxyManager {
|
|||||||
const requestsDayBtn = document.getElementById('requests-day-btn');
|
const requestsDayBtn = document.getElementById('requests-day-btn');
|
||||||
const tokensHourBtn = document.getElementById('tokens-hour-btn');
|
const tokensHourBtn = document.getElementById('tokens-hour-btn');
|
||||||
const tokensDayBtn = document.getElementById('tokens-day-btn');
|
const tokensDayBtn = document.getElementById('tokens-day-btn');
|
||||||
|
const costHourBtn = document.getElementById('cost-hour-btn');
|
||||||
|
const costDayBtn = document.getElementById('cost-day-btn');
|
||||||
const chartLineSelects = document.querySelectorAll('.chart-line-select');
|
const chartLineSelects = document.querySelectorAll('.chart-line-select');
|
||||||
const modelPriceForm = document.getElementById('model-price-form');
|
const modelPriceForm = document.getElementById('model-price-form');
|
||||||
const resetModelPricesBtn = document.getElementById('reset-model-prices');
|
const resetModelPricesBtn = document.getElementById('reset-model-prices');
|
||||||
@@ -523,6 +525,12 @@ class CLIProxyManager {
|
|||||||
if (tokensDayBtn) {
|
if (tokensDayBtn) {
|
||||||
tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day'));
|
tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day'));
|
||||||
}
|
}
|
||||||
|
if (costHourBtn) {
|
||||||
|
costHourBtn.addEventListener('click', () => this.switchCostPeriod('hour'));
|
||||||
|
}
|
||||||
|
if (costDayBtn) {
|
||||||
|
costDayBtn.addEventListener('click', () => this.switchCostPeriod('day'));
|
||||||
|
}
|
||||||
if (chartLineSelects.length) {
|
if (chartLineSelects.length) {
|
||||||
chartLineSelects.forEach(select => {
|
chartLineSelects.forEach(select => {
|
||||||
select.addEventListener('change', (event) => {
|
select.addEventListener('change', (event) => {
|
||||||
|
|||||||
12
index.html
12
index.html
@@ -997,9 +997,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card chart-card cost-chart-card">
|
<div class="card chart-card cost-chart-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3><i class="fas fa-sack-dollar"></i> <span data-i18n="usage_stats.cost_trend">花费统计</span></h3>
|
<h3><i class="fas fa-sack-dollar"></i> <span data-i18n="usage_stats.cost_trend">花费统计</span></h3>
|
||||||
|
<div class="chart-controls">
|
||||||
|
<button class="btn btn-small" data-period="hour" id="cost-hour-btn">
|
||||||
|
<span data-i18n="usage_stats.by_hour">按小时</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-small active" data-period="day" id="cost-day-btn">
|
||||||
|
<span data-i18n="usage_stats.by_day">按天</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<canvas id="cost-chart"></canvas>
|
<canvas id="cost-chart"></canvas>
|
||||||
|
|||||||
@@ -102,16 +102,18 @@ export async function loadUsageStats(usageData = null) {
|
|||||||
// 读取当前图表周期
|
// 读取当前图表周期
|
||||||
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
|
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
|
||||||
const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
|
const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
|
||||||
|
const costHourActive = document.getElementById('cost-hour-btn')?.classList.contains('active');
|
||||||
const requestsPeriod = requestsHourActive ? 'hour' : 'day';
|
const requestsPeriod = requestsHourActive ? 'hour' : 'day';
|
||||||
const tokensPeriod = tokensHourActive ? 'hour' : 'day';
|
const tokensPeriod = tokensHourActive ? 'hour' : 'day';
|
||||||
|
const costPeriod = costHourActive ? 'hour' : 'day';
|
||||||
|
|
||||||
// 初始化图表(使用当前周期)
|
// 初始化图表(使用当前周期)
|
||||||
this.initializeRequestsChart(requestsPeriod);
|
this.initializeRequestsChart(requestsPeriod);
|
||||||
this.initializeTokensChart(tokensPeriod);
|
this.initializeTokensChart(tokensPeriod);
|
||||||
|
this.updateCostSummaryAndChart(usage, costPeriod);
|
||||||
|
|
||||||
// 更新API详细统计表格
|
// 更新API详细统计表格
|
||||||
this.updateApiStatsTable(usage);
|
this.updateApiStatsTable(usage);
|
||||||
this.updateCostSummaryAndChart(usage);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载使用统计失败:', error);
|
console.error('加载使用统计失败:', error);
|
||||||
@@ -566,7 +568,7 @@ export function handleModelPriceSubmit() {
|
|||||||
next[model] = { prompt, completion };
|
next[model] = { prompt, completion };
|
||||||
this.persistModelPrices(next);
|
this.persistModelPrices(next);
|
||||||
this.renderSavedModelPrices();
|
this.renderSavedModelPrices();
|
||||||
this.updateCostSummaryAndChart();
|
this.updateCostSummaryAndChart(this.currentUsageData, this.getCostChartPeriod());
|
||||||
this.showNotification(i18n.t('usage_stats.model_price_saved'), 'success');
|
this.showNotification(i18n.t('usage_stats.model_price_saved'), 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +585,7 @@ export function handleModelPriceReset() {
|
|||||||
}
|
}
|
||||||
this.renderSavedModelPrices();
|
this.renderSavedModelPrices();
|
||||||
this.prefillModelPriceInputs();
|
this.prefillModelPriceInputs();
|
||||||
this.updateCostSummaryAndChart();
|
this.updateCostSummaryAndChart(this.currentUsageData, this.getCostChartPeriod());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateTokenBreakdown(usage = null) {
|
export function calculateTokenBreakdown(usage = null) {
|
||||||
@@ -825,7 +827,7 @@ export function formatUsd(value) {
|
|||||||
return `$${parts}`;
|
return `$${parts}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateCostData(prices = null, usage = null) {
|
export function calculateCostData(prices = null, usage = null, period = 'day') {
|
||||||
const priceTable = prices || this.modelPrices || {};
|
const priceTable = prices || this.modelPrices || {};
|
||||||
const usagePayload = usage || this.currentUsageData;
|
const usagePayload = usage || this.currentUsageData;
|
||||||
const entries = Object.entries(priceTable || {});
|
const entries = Object.entries(priceTable || {});
|
||||||
@@ -840,24 +842,11 @@ export function calculateCostData(prices = null, usage = null) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelSet = new Set();
|
const normalizedDetails = details.map(detail => {
|
||||||
const costByModelDay = new Map();
|
|
||||||
let totalCost = 0;
|
|
||||||
|
|
||||||
details.forEach(detail => {
|
|
||||||
const parsedTimestamp = Date.parse(detail.timestamp);
|
|
||||||
if (Number.isNaN(parsedTimestamp)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dayLabel = this.formatDayLabel(new Date(parsedTimestamp));
|
|
||||||
if (!dayLabel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelName = detail.__modelName || 'Unknown';
|
const modelName = detail.__modelName || 'Unknown';
|
||||||
const price = priceTable[modelName];
|
const price = priceTable[modelName];
|
||||||
if (!price) {
|
if (!price) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = detail?.tokens || {};
|
const tokens = detail?.tokens || {};
|
||||||
@@ -866,19 +855,66 @@ export function calculateCostData(prices = null, usage = null) {
|
|||||||
const promptCost = (promptTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.prompt) || 0);
|
const promptCost = (promptTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.prompt) || 0);
|
||||||
const completionCost = (completionTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.completion) || 0);
|
const completionCost = (completionTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.completion) || 0);
|
||||||
const detailCost = promptCost + completionCost;
|
const detailCost = promptCost + completionCost;
|
||||||
|
const parsedTimestamp = Date.parse(detail.timestamp);
|
||||||
|
|
||||||
if (!Number.isFinite(detailCost) || detailCost <= 0) {
|
if (!Number.isFinite(detailCost) || detailCost <= 0 || Number.isNaN(parsedTimestamp)) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalCost += detailCost;
|
return { modelName, cost: detailCost, timestamp: parsedTimestamp };
|
||||||
labelSet.add(dayLabel);
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
if (!normalizedDetails.length) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCost = normalizedDetails.reduce((sum, item) => sum + item.cost, 0);
|
||||||
|
|
||||||
|
if (period === 'hour') {
|
||||||
|
const meta = this.createHourlyBucketMeta();
|
||||||
|
const dataByModel = new Map();
|
||||||
|
|
||||||
|
normalizedDetails.forEach(({ modelName, cost, timestamp }) => {
|
||||||
|
const normalized = new Date(timestamp);
|
||||||
|
normalized.setMinutes(0, 0, 0);
|
||||||
|
const bucketStart = normalized.getTime();
|
||||||
|
if (bucketStart < meta.earliestTime || bucketStart > meta.lastBucketTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bucketIndex = Math.floor((bucketStart - meta.earliestTime) / meta.bucketSize);
|
||||||
|
if (bucketIndex < 0 || bucketIndex >= meta.labels.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!dataByModel.has(modelName)) {
|
||||||
|
dataByModel.set(modelName, new Array(meta.labels.length).fill(0));
|
||||||
|
}
|
||||||
|
const bucketValues = dataByModel.get(modelName);
|
||||||
|
bucketValues[bucketIndex] += cost;
|
||||||
|
});
|
||||||
|
|
||||||
|
const datasets = [];
|
||||||
|
dataByModel.forEach((series, modelName) => {
|
||||||
|
datasets.push({ label: modelName, data: series.map(value => Number(value.toFixed(4))) });
|
||||||
|
});
|
||||||
|
|
||||||
|
return { totalCost, labels: meta.labels, datasets };
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelSet = new Set();
|
||||||
|
const costByModelDay = new Map();
|
||||||
|
|
||||||
|
normalizedDetails.forEach(({ modelName, cost, timestamp }) => {
|
||||||
|
const dayLabel = this.formatDayLabel(new Date(timestamp));
|
||||||
|
if (!dayLabel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!costByModelDay.has(modelName)) {
|
if (!costByModelDay.has(modelName)) {
|
||||||
costByModelDay.set(modelName, new Map());
|
costByModelDay.set(modelName, new Map());
|
||||||
}
|
}
|
||||||
const dayMap = costByModelDay.get(modelName);
|
const dayMap = costByModelDay.get(modelName);
|
||||||
dayMap.set(dayLabel, (dayMap.get(dayLabel) || 0) + detailCost);
|
dayMap.set(dayLabel, (dayMap.get(dayLabel) || 0) + cost);
|
||||||
|
labelSet.add(dayLabel);
|
||||||
});
|
});
|
||||||
|
|
||||||
const labels = Array.from(labelSet).sort();
|
const labels = Array.from(labelSet).sort();
|
||||||
@@ -914,7 +950,7 @@ export function destroyCostChart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initializeCostChart(costData) {
|
export function initializeCostChart(costData, period = 'day') {
|
||||||
const canvas = document.getElementById('cost-chart');
|
const canvas = document.getElementById('cost-chart');
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
return;
|
return;
|
||||||
@@ -972,7 +1008,7 @@ export function initializeCostChart(costData) {
|
|||||||
x: {
|
x: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: i18n.t('usage_stats.by_day')
|
text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
@@ -999,11 +1035,17 @@ export function initializeCostChart(costData) {
|
|||||||
this.setCostChartPlaceholder(null);
|
this.setCostChartPlaceholder(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateCostSummaryAndChart(usage = null) {
|
export function getCostChartPeriod() {
|
||||||
|
const costHourActive = document.getElementById('cost-hour-btn')?.classList.contains('active');
|
||||||
|
return costHourActive ? 'hour' : 'day';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCostSummaryAndChart(usage = null, period = null) {
|
||||||
this.ensureModelPriceState();
|
this.ensureModelPriceState();
|
||||||
const totalCostEl = document.getElementById('total-cost');
|
const totalCostEl = document.getElementById('total-cost');
|
||||||
const hasPrices = Object.keys(this.modelPrices || {}).length > 0;
|
const hasPrices = Object.keys(this.modelPrices || {}).length > 0;
|
||||||
const usagePayload = usage || this.currentUsageData;
|
const usagePayload = usage || this.currentUsageData;
|
||||||
|
const resolvedPeriod = period || this.getCostChartPeriod();
|
||||||
|
|
||||||
if (!hasPrices) {
|
if (!hasPrices) {
|
||||||
if (totalCostEl) {
|
if (totalCostEl) {
|
||||||
@@ -1023,7 +1065,7 @@ export function updateCostSummaryAndChart(usage = null) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const costData = this.calculateCostData(this.modelPrices, usagePayload);
|
const costData = this.calculateCostData(this.modelPrices, usagePayload, resolvedPeriod);
|
||||||
if (totalCostEl) {
|
if (totalCostEl) {
|
||||||
totalCostEl.textContent = this.formatUsd(costData.totalCost);
|
totalCostEl.textContent = this.formatUsd(costData.totalCost);
|
||||||
}
|
}
|
||||||
@@ -1034,15 +1076,17 @@ export function updateCostSummaryAndChart(usage = null) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initializeCostChart(costData);
|
this.initializeCostChart(costData, resolvedPeriod);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化图表
|
// 初始化图表
|
||||||
export function initializeCharts() {
|
export function initializeCharts() {
|
||||||
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
|
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
|
||||||
const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
|
const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
|
||||||
|
const costHourActive = document.getElementById('cost-hour-btn')?.classList.contains('active');
|
||||||
this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day');
|
this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day');
|
||||||
this.initializeTokensChart(tokensHourActive ? 'hour' : 'day');
|
this.initializeTokensChart(tokensHourActive ? 'hour' : 'day');
|
||||||
|
this.updateCostSummaryAndChart(this.currentUsageData, costHourActive ? 'hour' : 'day');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化请求趋势图表
|
// 初始化请求趋势图表
|
||||||
@@ -1213,6 +1257,20 @@ export function switchTokensPeriod(period) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function switchCostPeriod(period) {
|
||||||
|
if (period !== 'hour' && period !== 'day') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hourBtn = document.getElementById('cost-hour-btn');
|
||||||
|
const dayBtn = document.getElementById('cost-day-btn');
|
||||||
|
if (hourBtn && dayBtn) {
|
||||||
|
hourBtn.classList.toggle('active', period === 'hour');
|
||||||
|
dayBtn.classList.toggle('active', period === 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCostSummaryAndChart(this.currentUsageData, period);
|
||||||
|
}
|
||||||
|
|
||||||
// 更新API详细统计表格
|
// 更新API详细统计表格
|
||||||
export function updateApiStatsTable(data) {
|
export function updateApiStatsTable(data) {
|
||||||
const container = document.getElementById('api-stats-table');
|
const container = document.getElementById('api-stats-table');
|
||||||
@@ -1312,6 +1370,7 @@ export const usageModule = {
|
|||||||
extractTotalTokens,
|
extractTotalTokens,
|
||||||
formatUsd,
|
formatUsd,
|
||||||
calculateCostData,
|
calculateCostData,
|
||||||
|
getCostChartPeriod,
|
||||||
setCostChartPlaceholder,
|
setCostChartPlaceholder,
|
||||||
destroyCostChart,
|
destroyCostChart,
|
||||||
initializeCostChart,
|
initializeCostChart,
|
||||||
@@ -1323,6 +1382,7 @@ export const usageModule = {
|
|||||||
getTokensChartData,
|
getTokensChartData,
|
||||||
switchRequestsPeriod,
|
switchRequestsPeriod,
|
||||||
switchTokensPeriod,
|
switchTokensPeriod,
|
||||||
|
switchCostPeriod,
|
||||||
updateApiStatsTable,
|
updateApiStatsTable,
|
||||||
registerUsageListeners
|
registerUsageListeners
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user