mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
feat(app.js, i18n, index.html): refactor model filtering to chart line selections
- Replaced the model filter dropdown with multiple chart line selectors for improved data visualization. - Updated event handling to manage changes in chart line selections and refresh chart data accordingly. - Enhanced internationalization strings for chart line labels in both English and Chinese. - Adjusted the HTML structure to accommodate the new chart line selection UI.
This commit is contained in:
379
app.js
379
app.js
@@ -754,7 +754,7 @@ 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 modelFilterSelect = document.getElementById('model-filter-select');
|
const chartLineSelects = document.querySelectorAll('.chart-line-select');
|
||||||
|
|
||||||
if (refreshUsageStats) {
|
if (refreshUsageStats) {
|
||||||
refreshUsageStats.addEventListener('click', () => this.loadUsageStats());
|
refreshUsageStats.addEventListener('click', () => this.loadUsageStats());
|
||||||
@@ -771,8 +771,13 @@ class CLIProxyManager {
|
|||||||
if (tokensDayBtn) {
|
if (tokensDayBtn) {
|
||||||
tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day'));
|
tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day'));
|
||||||
}
|
}
|
||||||
if (modelFilterSelect) {
|
if (chartLineSelects.length) {
|
||||||
modelFilterSelect.addEventListener('change', (e) => this.handleModelFilterChange(e.target.value));
|
chartLineSelects.forEach(select => {
|
||||||
|
select.addEventListener('change', (event) => {
|
||||||
|
const index = Number.parseInt(select.getAttribute('data-line-index'), 10);
|
||||||
|
this.handleChartLineSelectionChange(Number.isNaN(index) ? -1 : index, event.target.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模态框
|
// 模态框
|
||||||
@@ -5856,11 +5861,17 @@ class CLIProxyManager {
|
|||||||
|
|
||||||
// ===== 使用统计相关方法 =====
|
// ===== 使用统计相关方法 =====
|
||||||
|
|
||||||
// 初始化图表变量
|
// 使用统计状态
|
||||||
requestsChart = null;
|
requestsChart = null;
|
||||||
tokensChart = null;
|
tokensChart = null;
|
||||||
currentUsageData = null;
|
currentUsageData = null;
|
||||||
currentModelFilter = 'all';
|
chartLineSelections = ['none', 'none', 'none'];
|
||||||
|
chartLineSelectIds = ['chart-line-select-0', 'chart-line-select-1', 'chart-line-select-2'];
|
||||||
|
chartLineStyles = [
|
||||||
|
{ borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.15)' },
|
||||||
|
{ borderColor: '#a855f7', backgroundColor: 'rgba(168, 85, 247, 0.15)' },
|
||||||
|
{ borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.15)' }
|
||||||
|
];
|
||||||
|
|
||||||
// 获取API密钥的统计信息
|
// 获取API密钥的统计信息
|
||||||
async getKeyStats() {
|
async getKeyStats() {
|
||||||
@@ -5926,7 +5937,7 @@ class CLIProxyManager {
|
|||||||
|
|
||||||
// 更新概览卡片
|
// 更新概览卡片
|
||||||
this.updateUsageOverview(usage);
|
this.updateUsageOverview(usage);
|
||||||
this.updateModelFilterOptions(usage);
|
this.updateChartLineSelectors(usage);
|
||||||
|
|
||||||
// 读取当前图表周期
|
// 读取当前图表周期
|
||||||
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
|
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
|
||||||
@@ -5944,7 +5955,7 @@ class CLIProxyManager {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载使用统计失败:', error);
|
console.error('加载使用统计失败:', error);
|
||||||
this.currentUsageData = null;
|
this.currentUsageData = null;
|
||||||
this.updateModelFilterOptions(null);
|
this.updateChartLineSelectors(null);
|
||||||
|
|
||||||
// 清空概览数据
|
// 清空概览数据
|
||||||
['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => {
|
['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => {
|
||||||
@@ -5995,50 +6006,92 @@ class CLIProxyManager {
|
|||||||
return Array.from(names).sort((a, b) => a.localeCompare(b));
|
return Array.from(names).sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateModelFilterOptions(usage) {
|
updateChartLineSelectors(usage) {
|
||||||
const select = document.getElementById('model-filter-select');
|
const modelNames = this.getModelNamesFromUsage(usage);
|
||||||
if (!select) {
|
const selectors = this.chartLineSelectIds
|
||||||
|
.map(id => document.getElementById(id))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (!selectors.length) {
|
||||||
|
this.chartLineSelections = ['none', 'none', 'none'];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelNames = this.getModelNamesFromUsage(usage);
|
const optionsFragment = () => {
|
||||||
const previousSelection = this.currentModelFilter || 'all';
|
const fragment = document.createDocumentFragment();
|
||||||
const fragment = document.createDocumentFragment();
|
const hiddenOption = document.createElement('option');
|
||||||
|
hiddenOption.value = 'none';
|
||||||
|
hiddenOption.textContent = i18n.t('usage_stats.chart_line_hidden');
|
||||||
|
fragment.appendChild(hiddenOption);
|
||||||
|
modelNames.forEach(name => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = name;
|
||||||
|
option.textContent = name;
|
||||||
|
fragment.appendChild(option);
|
||||||
|
});
|
||||||
|
return fragment;
|
||||||
|
};
|
||||||
|
|
||||||
const allOption = document.createElement('option');
|
const hasModels = modelNames.length > 0;
|
||||||
allOption.value = 'all';
|
selectors.forEach(select => {
|
||||||
allOption.textContent = i18n.t('usage_stats.model_filter_all');
|
select.innerHTML = '';
|
||||||
fragment.appendChild(allOption);
|
select.appendChild(optionsFragment());
|
||||||
|
select.disabled = !hasModels;
|
||||||
modelNames.forEach(name => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = name;
|
|
||||||
option.textContent = name;
|
|
||||||
fragment.appendChild(option);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
select.innerHTML = '';
|
if (!hasModels) {
|
||||||
select.appendChild(fragment);
|
this.chartLineSelections = ['none', 'none', 'none'];
|
||||||
|
selectors.forEach(select => {
|
||||||
let nextSelection = previousSelection;
|
select.value = 'none';
|
||||||
if (nextSelection !== 'all' && !modelNames.includes(nextSelection)) {
|
});
|
||||||
nextSelection = 'all';
|
|
||||||
}
|
|
||||||
this.currentModelFilter = nextSelection;
|
|
||||||
select.value = nextSelection;
|
|
||||||
select.disabled = modelNames.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModelFilterChange(value) {
|
|
||||||
const normalized = value || 'all';
|
|
||||||
if (this.currentModelFilter === normalized) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentModelFilter = normalized;
|
|
||||||
this.refreshChartsForModelFilter();
|
const nextSelections = Array.isArray(this.chartLineSelections)
|
||||||
|
? [...this.chartLineSelections]
|
||||||
|
: ['none', 'none', 'none'];
|
||||||
|
|
||||||
|
const validNames = new Set(modelNames);
|
||||||
|
let hasActiveSelection = false;
|
||||||
|
for (let i = 0; i < nextSelections.length; i++) {
|
||||||
|
const selection = nextSelections[i];
|
||||||
|
if (selection && selection !== 'none' && !validNames.has(selection)) {
|
||||||
|
nextSelections[i] = 'none';
|
||||||
|
}
|
||||||
|
if (nextSelections[i] !== 'none') {
|
||||||
|
hasActiveSelection = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasActiveSelection) {
|
||||||
|
modelNames.slice(0, nextSelections.length).forEach((name, index) => {
|
||||||
|
nextSelections[index] = name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartLineSelections = nextSelections;
|
||||||
|
selectors.forEach((select, index) => {
|
||||||
|
const value = this.chartLineSelections[index] || 'none';
|
||||||
|
select.value = value;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshChartsForModelFilter() {
|
handleChartLineSelectionChange(index, value) {
|
||||||
|
if (!Array.isArray(this.chartLineSelections)) {
|
||||||
|
this.chartLineSelections = ['none', 'none', 'none'];
|
||||||
|
}
|
||||||
|
if (index < 0 || index >= this.chartLineSelections.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = value || 'none';
|
||||||
|
if (this.chartLineSelections[index] === normalized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.chartLineSelections[index] = normalized;
|
||||||
|
this.refreshChartsForSelections();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshChartsForSelections() {
|
||||||
if (!this.currentUsageData) {
|
if (!this.currentUsageData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -6062,6 +6115,15 @@ class CLIProxyManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getActiveChartLineSelections() {
|
||||||
|
if (!Array.isArray(this.chartLineSelections)) {
|
||||||
|
this.chartLineSelections = ['none', 'none', 'none'];
|
||||||
|
}
|
||||||
|
return this.chartLineSelections
|
||||||
|
.map((value, index) => ({ model: value, index }))
|
||||||
|
.filter(item => item.model && item.model !== 'none');
|
||||||
|
}
|
||||||
|
|
||||||
// 收集所有请求明细,供图表等复用
|
// 收集所有请求明细,供图表等复用
|
||||||
collectUsageDetailsFromUsage(usage) {
|
collectUsageDetailsFromUsage(usage) {
|
||||||
if (!usage) {
|
if (!usage) {
|
||||||
@@ -6090,14 +6152,7 @@ class CLIProxyManager {
|
|||||||
return this.collectUsageDetailsFromUsage(this.currentUsageData);
|
return this.collectUsageDetailsFromUsage(this.currentUsageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建最近24小时的统计序列
|
createHourlyBucketMeta() {
|
||||||
buildRecentHourlySeries(metric = 'requests') {
|
|
||||||
const details = this.collectUsageDetails();
|
|
||||||
if (!details.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelFilter = this.currentModelFilter || 'all';
|
|
||||||
const hourMs = 60 * 60 * 1000;
|
const hourMs = 60 * 60 * 1000;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const currentHour = new Date(now);
|
const currentHour = new Date(now);
|
||||||
@@ -6107,20 +6162,30 @@ class CLIProxyManager {
|
|||||||
earliestBucket.setHours(earliestBucket.getHours() - 23);
|
earliestBucket.setHours(earliestBucket.getHours() - 23);
|
||||||
const earliestTime = earliestBucket.getTime();
|
const earliestTime = earliestBucket.getTime();
|
||||||
const labels = [];
|
const labels = [];
|
||||||
const values = new Array(24).fill(0);
|
|
||||||
|
|
||||||
for (let i = 0; i < 24; i++) {
|
for (let i = 0; i < 24; i++) {
|
||||||
const bucketStart = earliestTime + i * hourMs;
|
const bucketStart = earliestTime + i * hourMs;
|
||||||
labels.push(this.formatHourLabel(new Date(bucketStart)));
|
labels.push(this.formatHourLabel(new Date(bucketStart)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestBucketStart = earliestTime + (values.length - 1) * hourMs;
|
return {
|
||||||
let hasMatch = false;
|
labels,
|
||||||
|
earliestTime,
|
||||||
|
bucketSize: hourMs,
|
||||||
|
lastBucketTime: earliestTime + (labels.length - 1) * hourMs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildHourlySeriesByModel(metric = 'requests') {
|
||||||
|
const meta = this.createHourlyBucketMeta();
|
||||||
|
const details = this.collectUsageDetails();
|
||||||
|
const dataByModel = new Map();
|
||||||
|
let hasData = false;
|
||||||
|
|
||||||
|
if (!details.length) {
|
||||||
|
return { labels: meta.labels, dataByModel, hasData };
|
||||||
|
}
|
||||||
|
|
||||||
details.forEach(detail => {
|
details.forEach(detail => {
|
||||||
if (modelFilter !== 'all' && detail.__modelName !== modelFilter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const timestamp = Date.parse(detail.timestamp);
|
const timestamp = Date.parse(detail.timestamp);
|
||||||
if (Number.isNaN(timestamp)) {
|
if (Number.isNaN(timestamp)) {
|
||||||
return;
|
return;
|
||||||
@@ -6129,44 +6194,43 @@ class CLIProxyManager {
|
|||||||
const normalized = new Date(timestamp);
|
const normalized = new Date(timestamp);
|
||||||
normalized.setMinutes(0, 0, 0);
|
normalized.setMinutes(0, 0, 0);
|
||||||
const bucketStart = normalized.getTime();
|
const bucketStart = normalized.getTime();
|
||||||
if (bucketStart < earliestTime || bucketStart > latestBucketStart) {
|
if (bucketStart < meta.earliestTime || bucketStart > meta.lastBucketTime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bucketIndex = Math.floor((bucketStart - earliestTime) / hourMs);
|
const bucketIndex = Math.floor((bucketStart - meta.earliestTime) / meta.bucketSize);
|
||||||
if (bucketIndex < 0 || bucketIndex >= values.length) {
|
if (bucketIndex < 0 || bucketIndex >= meta.labels.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modelName = detail.__modelName || 'Unknown';
|
||||||
|
if (!dataByModel.has(modelName)) {
|
||||||
|
dataByModel.set(modelName, new Array(meta.labels.length).fill(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucketValues = dataByModel.get(modelName);
|
||||||
if (metric === 'tokens') {
|
if (metric === 'tokens') {
|
||||||
values[bucketIndex] += this.extractTotalTokens(detail);
|
bucketValues[bucketIndex] += this.extractTotalTokens(detail);
|
||||||
} else {
|
} else {
|
||||||
values[bucketIndex] += 1;
|
bucketValues[bucketIndex] += 1;
|
||||||
}
|
}
|
||||||
hasMatch = true;
|
hasData = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasMatch) {
|
return { labels: meta.labels, dataByModel, hasData };
|
||||||
return modelFilter === 'all' ? null : { labels, values };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { labels, values };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDailySeries(metric = 'requests') {
|
buildDailySeriesByModel(metric = 'requests') {
|
||||||
const details = this.collectUsageDetails();
|
const details = this.collectUsageDetails();
|
||||||
|
const valuesByModel = new Map();
|
||||||
|
const labelsSet = new Set();
|
||||||
|
let hasData = false;
|
||||||
|
|
||||||
if (!details.length) {
|
if (!details.length) {
|
||||||
return null;
|
return { labels: [], dataByModel: new Map(), hasData };
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelFilter = this.currentModelFilter || 'all';
|
|
||||||
const dayBuckets = {};
|
|
||||||
let hasMatch = false;
|
|
||||||
|
|
||||||
details.forEach(detail => {
|
details.forEach(detail => {
|
||||||
if (modelFilter !== 'all' && detail.__modelName !== modelFilter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const timestamp = Date.parse(detail.timestamp);
|
const timestamp = Date.parse(detail.timestamp);
|
||||||
if (Number.isNaN(timestamp)) {
|
if (Number.isNaN(timestamp)) {
|
||||||
return;
|
return;
|
||||||
@@ -6176,24 +6240,53 @@ class CLIProxyManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dayBuckets[dayLabel]) {
|
const modelName = detail.__modelName || 'Unknown';
|
||||||
dayBuckets[dayLabel] = 0;
|
if (!valuesByModel.has(modelName)) {
|
||||||
|
valuesByModel.set(modelName, new Map());
|
||||||
}
|
}
|
||||||
if (metric === 'tokens') {
|
const modelDayMap = valuesByModel.get(modelName);
|
||||||
dayBuckets[dayLabel] += this.extractTotalTokens(detail);
|
const increment = metric === 'tokens' ? this.extractTotalTokens(detail) : 1;
|
||||||
} else {
|
modelDayMap.set(dayLabel, (modelDayMap.get(dayLabel) || 0) + increment);
|
||||||
dayBuckets[dayLabel] += 1;
|
labelsSet.add(dayLabel);
|
||||||
}
|
hasData = true;
|
||||||
hasMatch = true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasMatch) {
|
const labels = Array.from(labelsSet).sort();
|
||||||
return modelFilter === 'all' ? null : { labels: [], values: [] };
|
const dataByModel = new Map();
|
||||||
}
|
valuesByModel.forEach((dayMap, modelName) => {
|
||||||
|
const series = labels.map(label => dayMap.get(label) || 0);
|
||||||
|
dataByModel.set(modelName, series);
|
||||||
|
});
|
||||||
|
|
||||||
const labels = Object.keys(dayBuckets).sort();
|
return { labels, dataByModel, hasData };
|
||||||
const values = labels.map(label => dayBuckets[label] || 0);
|
}
|
||||||
return { labels, values };
|
|
||||||
|
buildChartDataForMetric(period = 'day', metric = 'requests') {
|
||||||
|
const baseSeries = period === 'hour'
|
||||||
|
? this.buildHourlySeriesByModel(metric)
|
||||||
|
: this.buildDailySeriesByModel(metric);
|
||||||
|
|
||||||
|
const labels = baseSeries?.labels || [];
|
||||||
|
const dataByModel = baseSeries?.dataByModel || new Map();
|
||||||
|
const activeSelections = this.getActiveChartLineSelections();
|
||||||
|
const datasets = activeSelections.map(selection => {
|
||||||
|
const values = dataByModel.get(selection.model) || new Array(labels.length).fill(0);
|
||||||
|
const style = this.chartLineStyles[selection.index] || this.chartLineStyles[0];
|
||||||
|
return {
|
||||||
|
label: selection.model,
|
||||||
|
data: values,
|
||||||
|
borderColor: style.borderColor,
|
||||||
|
backgroundColor: style.backgroundColor,
|
||||||
|
fill: false,
|
||||||
|
tension: 0.35,
|
||||||
|
pointBackgroundColor: style.borderColor,
|
||||||
|
pointBorderColor: '#ffffff',
|
||||||
|
pointBorderWidth: 2,
|
||||||
|
pointRadius: values.some(v => v > 0) ? 4 : 3
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { labels, datasets };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一格式化小时标签
|
// 统一格式化小时标签
|
||||||
@@ -6255,9 +6348,18 @@ class CLIProxyManager {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: true,
|
||||||
|
position: 'top',
|
||||||
|
align: 'start',
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
@@ -6277,14 +6379,10 @@ class CLIProxyManager {
|
|||||||
},
|
},
|
||||||
elements: {
|
elements: {
|
||||||
line: {
|
line: {
|
||||||
borderColor: '#3b82f6',
|
tension: 0.35,
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
borderWidth: 2
|
||||||
fill: true,
|
|
||||||
tension: 0.4
|
|
||||||
},
|
},
|
||||||
point: {
|
point: {
|
||||||
backgroundColor: '#3b82f6',
|
|
||||||
borderColor: '#ffffff',
|
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
radius: 4
|
radius: 4
|
||||||
}
|
}
|
||||||
@@ -6311,9 +6409,18 @@ class CLIProxyManager {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: true,
|
||||||
|
position: 'top',
|
||||||
|
align: 'start',
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
@@ -6333,14 +6440,10 @@ class CLIProxyManager {
|
|||||||
},
|
},
|
||||||
elements: {
|
elements: {
|
||||||
line: {
|
line: {
|
||||||
borderColor: '#10b981',
|
tension: 0.35,
|
||||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
borderWidth: 2
|
||||||
fill: true,
|
|
||||||
tension: 0.4
|
|
||||||
},
|
},
|
||||||
point: {
|
point: {
|
||||||
backgroundColor: '#10b981',
|
|
||||||
borderColor: '#ffffff',
|
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
radius: 4
|
radius: 4
|
||||||
}
|
}
|
||||||
@@ -6352,77 +6455,17 @@ class CLIProxyManager {
|
|||||||
// 获取请求图表数据
|
// 获取请求图表数据
|
||||||
getRequestsChartData(period) {
|
getRequestsChartData(period) {
|
||||||
if (!this.currentUsageData) {
|
if (!this.currentUsageData) {
|
||||||
return { labels: [], datasets: [{ data: [] }] };
|
return { labels: [], datasets: [] };
|
||||||
}
|
}
|
||||||
|
return this.buildChartDataForMetric(period, 'requests');
|
||||||
let dataSource, labels, values;
|
|
||||||
|
|
||||||
if (period === 'hour') {
|
|
||||||
const hourlySeries = this.buildRecentHourlySeries('requests');
|
|
||||||
if (hourlySeries) {
|
|
||||||
labels = hourlySeries.labels;
|
|
||||||
values = hourlySeries.values;
|
|
||||||
} else {
|
|
||||||
dataSource = this.currentUsageData.requests_by_hour || {};
|
|
||||||
labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
|
|
||||||
values = labels.map(hour => dataSource[hour] || 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const dailySeries = this.buildDailySeries('requests');
|
|
||||||
if (dailySeries) {
|
|
||||||
labels = dailySeries.labels;
|
|
||||||
values = dailySeries.values;
|
|
||||||
} else {
|
|
||||||
dataSource = this.currentUsageData.requests_by_day || {};
|
|
||||||
labels = Object.keys(dataSource).sort();
|
|
||||||
values = labels.map(day => dataSource[day] || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [{
|
|
||||||
data: values
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取Token图表数据
|
// 获取Token图表数据
|
||||||
getTokensChartData(period) {
|
getTokensChartData(period) {
|
||||||
if (!this.currentUsageData) {
|
if (!this.currentUsageData) {
|
||||||
return { labels: [], datasets: [{ data: [] }] };
|
return { labels: [], datasets: [] };
|
||||||
}
|
}
|
||||||
|
return this.buildChartDataForMetric(period, 'tokens');
|
||||||
let dataSource, labels, values;
|
|
||||||
|
|
||||||
if (period === 'hour') {
|
|
||||||
const hourlySeries = this.buildRecentHourlySeries('tokens');
|
|
||||||
if (hourlySeries) {
|
|
||||||
labels = hourlySeries.labels;
|
|
||||||
values = hourlySeries.values;
|
|
||||||
} else {
|
|
||||||
dataSource = this.currentUsageData.tokens_by_hour || {};
|
|
||||||
labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
|
|
||||||
values = labels.map(hour => dataSource[hour] || 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const dailySeries = this.buildDailySeries('tokens');
|
|
||||||
if (dailySeries) {
|
|
||||||
labels = dailySeries.labels;
|
|
||||||
values = dailySeries.values;
|
|
||||||
} else {
|
|
||||||
dataSource = this.currentUsageData.tokens_by_day || {};
|
|
||||||
labels = Object.keys(dataSource).sort();
|
|
||||||
values = labels.map(day => dataSource[day] || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [{
|
|
||||||
data: values
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换请求图表时间周期
|
// 切换请求图表时间周期
|
||||||
|
|||||||
12
i18n.js
12
i18n.js
@@ -374,8 +374,10 @@ const i18n = {
|
|||||||
'usage_stats.by_hour': '按小时',
|
'usage_stats.by_hour': '按小时',
|
||||||
'usage_stats.by_day': '按天',
|
'usage_stats.by_day': '按天',
|
||||||
'usage_stats.refresh': '刷新',
|
'usage_stats.refresh': '刷新',
|
||||||
'usage_stats.model_filter_label': '模型筛选',
|
'usage_stats.chart_line_label_1': '曲线 1',
|
||||||
'usage_stats.model_filter_all': '所有模型',
|
'usage_stats.chart_line_label_2': '曲线 2',
|
||||||
|
'usage_stats.chart_line_label_3': '曲线 3',
|
||||||
|
'usage_stats.chart_line_hidden': '不显示',
|
||||||
'usage_stats.no_data': '暂无数据',
|
'usage_stats.no_data': '暂无数据',
|
||||||
'usage_stats.loading_error': '加载失败',
|
'usage_stats.loading_error': '加载失败',
|
||||||
'usage_stats.api_endpoint': 'API端点',
|
'usage_stats.api_endpoint': 'API端点',
|
||||||
@@ -877,8 +879,10 @@ const i18n = {
|
|||||||
'usage_stats.by_hour': 'By Hour',
|
'usage_stats.by_hour': 'By Hour',
|
||||||
'usage_stats.by_day': 'By Day',
|
'usage_stats.by_day': 'By Day',
|
||||||
'usage_stats.refresh': 'Refresh',
|
'usage_stats.refresh': 'Refresh',
|
||||||
'usage_stats.model_filter_label': 'Model Filter',
|
'usage_stats.chart_line_label_1': 'Line 1',
|
||||||
'usage_stats.model_filter_all': 'All Models',
|
'usage_stats.chart_line_label_2': 'Line 2',
|
||||||
|
'usage_stats.chart_line_label_3': 'Line 3',
|
||||||
|
'usage_stats.chart_line_hidden': 'Hide',
|
||||||
'usage_stats.no_data': 'No Data Available',
|
'usage_stats.no_data': 'No Data Available',
|
||||||
'usage_stats.loading_error': 'Loading Failed',
|
'usage_stats.loading_error': 'Loading Failed',
|
||||||
'usage_stats.api_endpoint': 'API Endpoint',
|
'usage_stats.api_endpoint': 'API Endpoint',
|
||||||
|
|||||||
20
index.html
20
index.html
@@ -825,12 +825,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模型筛选 -->
|
<!-- 图表曲线选择 -->
|
||||||
<div class="usage-filter-bar">
|
<div class="usage-filter-bar">
|
||||||
<div class="usage-filter-group">
|
<div class="usage-filter-group">
|
||||||
<label for="model-filter-select" data-i18n="usage_stats.model_filter_label">模型筛选</label>
|
<label for="chart-line-select-0" data-i18n="usage_stats.chart_line_label_1">曲线 1</label>
|
||||||
<select id="model-filter-select" class="model-filter-select" disabled>
|
<select id="chart-line-select-0" class="model-filter-select chart-line-select" data-line-index="0" disabled>
|
||||||
<option value="all" data-i18n="usage_stats.model_filter_all">所有模型</option>
|
<option value="none" data-i18n="usage_stats.chart_line_hidden">不显示</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="usage-filter-group">
|
||||||
|
<label for="chart-line-select-1" data-i18n="usage_stats.chart_line_label_2">曲线 2</label>
|
||||||
|
<select id="chart-line-select-1" class="model-filter-select chart-line-select" data-line-index="1" disabled>
|
||||||
|
<option value="none" data-i18n="usage_stats.chart_line_hidden">不显示</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="usage-filter-group">
|
||||||
|
<label for="chart-line-select-2" data-i18n="usage_stats.chart_line_label_3">曲线 3</label>
|
||||||
|
<select id="chart-line-select-2" class="model-filter-select chart-line-select" data-line-index="2" disabled>
|
||||||
|
<option value="none" data-i18n="usage_stats.chart_line_hidden">不显示</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user