diff --git a/app.js b/app.js
index 04598fd..1e187d6 100644
--- a/app.js
+++ b/app.js
@@ -524,6 +524,8 @@ class CLIProxyManager {
const tokensDayBtn = document.getElementById('tokens-day-btn');
const costHourBtn = document.getElementById('cost-hour-btn');
const costDayBtn = document.getElementById('cost-day-btn');
+ const addChartLineBtn = document.getElementById('add-chart-line');
+ const removeChartLineBtn = document.getElementById('remove-chart-line');
const chartLineSelects = document.querySelectorAll('.chart-line-select');
const modelPriceForm = document.getElementById('model-price-form');
const resetModelPricesBtn = document.getElementById('reset-model-prices');
@@ -550,6 +552,12 @@ class CLIProxyManager {
if (costDayBtn) {
costDayBtn.addEventListener('click', () => this.switchCostPeriod('day'));
}
+ if (addChartLineBtn) {
+ addChartLineBtn.addEventListener('click', () => this.changeChartLineCount(1));
+ }
+ if (removeChartLineBtn) {
+ removeChartLineBtn.addEventListener('click', () => this.changeChartLineCount(-1));
+ }
if (chartLineSelects.length) {
chartLineSelects.forEach(select => {
select.addEventListener('change', (event) => {
@@ -558,6 +566,7 @@ class CLIProxyManager {
});
});
}
+ this.updateChartLineControlsUI();
if (modelPriceForm) {
modelPriceForm.addEventListener('submit', (event) => {
event.preventDefault();
@@ -661,12 +670,20 @@ class CLIProxyManager {
tokensChart = null;
costChart = null;
currentUsageData = null;
- chartLineSelections = ['none', 'none', 'none'];
- chartLineSelectIds = ['chart-line-select-0', 'chart-line-select-1', 'chart-line-select-2'];
+ chartLineMaxCount = 9;
+ chartLineVisibleCount = 3;
+ chartLineSelections = Array(3).fill('none');
+ chartLineSelectIds = Array.from({ length: 9 }, (_, idx) => `chart-line-select-${idx}`);
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)' }
+ { borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.15)' },
+ { borderColor: '#f97316', backgroundColor: 'rgba(249, 115, 22, 0.15)' },
+ { borderColor: '#ec4899', backgroundColor: 'rgba(236, 72, 153, 0.15)' },
+ { borderColor: '#14b8a6', backgroundColor: 'rgba(20, 184, 166, 0.15)' },
+ { borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.15)' },
+ { borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.15)' },
+ { borderColor: '#22c55e', backgroundColor: 'rgba(34, 197, 94, 0.15)' }
];
modelPriceStorageKey = 'cli-proxy-model-prices-v2';
modelPrices = {};
diff --git a/i18n.js b/i18n.js
index 7b8b856..a32e1fd 100644
--- a/i18n.js
+++ b/i18n.js
@@ -463,7 +463,17 @@ const i18n = {
'usage_stats.chart_line_label_1': '曲线 1',
'usage_stats.chart_line_label_2': '曲线 2',
'usage_stats.chart_line_label_3': '曲线 3',
+ 'usage_stats.chart_line_label_4': '曲线 4',
+ 'usage_stats.chart_line_label_5': '曲线 5',
+ 'usage_stats.chart_line_label_6': '曲线 6',
+ 'usage_stats.chart_line_label_7': '曲线 7',
+ 'usage_stats.chart_line_label_8': '曲线 8',
+ 'usage_stats.chart_line_label_9': '曲线 9',
'usage_stats.chart_line_hidden': '不显示',
+ 'usage_stats.chart_line_actions_label': '曲线数量',
+ 'usage_stats.chart_line_add': '增加曲线',
+ 'usage_stats.chart_line_remove': '减少曲线',
+ 'usage_stats.chart_line_hint': '最多同时显示 9 条模型曲线',
'usage_stats.no_data': '暂无数据',
'usage_stats.loading_error': '加载失败',
'usage_stats.api_endpoint': 'API端点',
@@ -1084,7 +1094,17 @@ const i18n = {
'usage_stats.chart_line_label_1': 'Line 1',
'usage_stats.chart_line_label_2': 'Line 2',
'usage_stats.chart_line_label_3': 'Line 3',
+ 'usage_stats.chart_line_label_4': 'Line 4',
+ 'usage_stats.chart_line_label_5': 'Line 5',
+ 'usage_stats.chart_line_label_6': 'Line 6',
+ 'usage_stats.chart_line_label_7': 'Line 7',
+ 'usage_stats.chart_line_label_8': 'Line 8',
+ 'usage_stats.chart_line_label_9': 'Line 9',
'usage_stats.chart_line_hidden': 'Hide',
+ 'usage_stats.chart_line_actions_label': 'Lines to display',
+ 'usage_stats.chart_line_add': 'Add line',
+ 'usage_stats.chart_line_remove': 'Remove line',
+ 'usage_stats.chart_line_hint': 'Show up to 9 model lines at once',
'usage_stats.no_data': 'No Data Available',
'usage_stats.loading_error': 'Loading Failed',
'usage_stats.api_endpoint': 'API Endpoint',
diff --git a/index.html b/index.html
index 5c66ed6..4f78cad 100644
--- a/index.html
+++ b/index.html
@@ -958,25 +958,76 @@
-
-
+
+
+
+
+
+
+ 3/9
+
+
最多显示 9 条模型曲线
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/usage.js b/src/modules/usage.js
index aa1e523..1380e79 100644
--- a/src/modules/usage.js
+++ b/src/modules/usage.js
@@ -1,6 +1,8 @@
const DEFAULT_MODEL_PRICE_STORAGE_KEY = 'cli-proxy-model-prices-v2';
const LEGACY_MODEL_PRICE_STORAGE_KEY = 'cli-proxy-model-prices';
const TOKENS_PER_PRICE_UNIT = 1_000_000;
+const DEFAULT_CHART_LINE_COUNT = 3;
+const MIN_CHART_LINE_COUNT = 1;
// 获取API密钥的统计信息
export async function getKeyStats(usageData = null) {
@@ -214,14 +216,101 @@ export function getModelNamesFromUsage(usage) {
return Array.from(names).sort((a, b) => a.localeCompare(b));
}
+export function getChartLineMaxCount() {
+ const idCount = Array.isArray(this.chartLineSelectIds) ? this.chartLineSelectIds.length : 0;
+ const configuredMax = Number(this.chartLineMaxCount);
+ const fallback = idCount || DEFAULT_CHART_LINE_COUNT;
+ const resolvedMax = Number.isFinite(configuredMax) ? configuredMax : fallback;
+ if (idCount > 0) {
+ return Math.max(MIN_CHART_LINE_COUNT, Math.min(resolvedMax, idCount));
+ }
+ return Math.max(MIN_CHART_LINE_COUNT, resolvedMax);
+}
+
+export function getVisibleChartLineCount() {
+ const maxCount = this.getChartLineMaxCount();
+ const stored = Number(this.chartLineVisibleCount);
+ const base = Number.isFinite(stored)
+ ? stored
+ : (Array.isArray(this.chartLineSelections) ? this.chartLineSelections.length : DEFAULT_CHART_LINE_COUNT);
+ const resolved = Math.min(Math.max(base, MIN_CHART_LINE_COUNT), maxCount);
+ this.chartLineVisibleCount = resolved;
+ return resolved;
+}
+
+export function ensureChartLineSelectionLength(targetLength = null) {
+ const maxCount = this.getChartLineMaxCount();
+ const desiredLength = Math.min(
+ Math.max(targetLength ?? this.getVisibleChartLineCount(), MIN_CHART_LINE_COUNT),
+ maxCount
+ );
+
+ if (!Array.isArray(this.chartLineSelections)) {
+ this.chartLineSelections = Array(desiredLength).fill('none');
+ return this.chartLineSelections;
+ }
+
+ const trimmed = this.chartLineSelections.slice(0, maxCount);
+ if (trimmed.length < desiredLength) {
+ this.chartLineSelections = [...trimmed, ...Array(desiredLength - trimmed.length).fill('none')];
+ } else if (trimmed.length > desiredLength) {
+ this.chartLineSelections = trimmed.slice(0, desiredLength);
+ } else {
+ this.chartLineSelections = trimmed;
+ }
+ return this.chartLineSelections;
+}
+
+export function updateChartLineControlsUI() {
+ const maxCount = this.getChartLineMaxCount();
+ const visibleCount = this.getVisibleChartLineCount();
+ const counter = document.getElementById('chart-line-count');
+ if (counter) {
+ counter.textContent = `${visibleCount}/${maxCount}`;
+ }
+ const addBtn = document.getElementById('add-chart-line');
+ const removeBtn = document.getElementById('remove-chart-line');
+ if (addBtn) {
+ addBtn.disabled = visibleCount >= maxCount;
+ }
+ if (removeBtn) {
+ removeBtn.disabled = visibleCount <= MIN_CHART_LINE_COUNT;
+ }
+}
+
+export function setChartLineVisibleCount(count) {
+ const maxCount = this.getChartLineMaxCount();
+ const nextCount = Math.min(Math.max(count, MIN_CHART_LINE_COUNT), maxCount);
+ const current = this.getVisibleChartLineCount();
+ if (nextCount === current) {
+ this.updateChartLineControlsUI();
+ return;
+ }
+ this.chartLineVisibleCount = nextCount;
+ this.ensureChartLineSelectionLength(nextCount);
+ this.updateChartLineSelectors(this.currentUsageData);
+ this.refreshChartsForSelections();
+}
+
+export function changeChartLineCount(delta = 0) {
+ const current = this.getVisibleChartLineCount();
+ this.setChartLineVisibleCount(current + delta);
+}
+
export function updateChartLineSelectors(usage) {
const modelNames = this.getModelNamesFromUsage(usage);
const selectors = this.chartLineSelectIds
.map(id => document.getElementById(id))
.filter(Boolean);
+ const availableCount = selectors.length || this.getChartLineMaxCount();
+ const visibleCount = Math.min(this.getVisibleChartLineCount(), availableCount);
+ this.chartLineVisibleCount = visibleCount;
+ this.ensureChartLineSelectionLength(visibleCount);
+
if (!selectors.length) {
- this.chartLineSelections = ['none', 'none', 'none'];
+ this.chartLineSelections = Array(visibleCount).fill('none');
+ this.updateChartLineControlsUI();
return;
}
@@ -241,23 +330,34 @@ export function updateChartLineSelectors(usage) {
};
const hasModels = modelNames.length > 0;
- selectors.forEach(select => {
+ selectors.forEach((select, index) => {
+ const group = select.closest('.chart-line-group');
+ const isVisible = index < visibleCount;
+ if (group) {
+ group.classList.toggle('chart-line-hidden', !isVisible);
+ }
select.innerHTML = '';
select.appendChild(optionsFragment());
- select.disabled = !hasModels;
+ select.disabled = !hasModels || !isVisible;
+ if (!isVisible) {
+ select.value = 'none';
+ }
});
if (!hasModels) {
- this.chartLineSelections = ['none', 'none', 'none'];
- selectors.forEach(select => {
+ this.chartLineSelections = Array(visibleCount).fill('none');
+ selectors.forEach((select, index) => {
+ const group = select.closest('.chart-line-group');
+ if (group) {
+ group.classList.toggle('chart-line-hidden', index >= visibleCount);
+ }
select.value = 'none';
});
+ this.updateChartLineControlsUI();
return;
}
- const nextSelections = Array.isArray(this.chartLineSelections)
- ? [...this.chartLineSelections]
- : ['none', 'none', 'none'];
+ const nextSelections = this.ensureChartLineSelectionLength(visibleCount).slice(0, visibleCount);
const validNames = new Set(modelNames);
let hasActiveSelection = false;
@@ -280,17 +380,17 @@ export function updateChartLineSelectors(usage) {
this.chartLineSelections = nextSelections;
selectors.forEach((select, index) => {
const value = this.chartLineSelections[index] || 'none';
- select.value = value;
+ select.value = index < visibleCount ? value : 'none';
});
+ this.updateChartLineControlsUI();
}
export function handleChartLineSelectionChange(index, value) {
- if (!Array.isArray(this.chartLineSelections)) {
- this.chartLineSelections = ['none', 'none', 'none'];
- }
- if (index < 0 || index >= this.chartLineSelections.length) {
+ const visibleCount = this.getVisibleChartLineCount();
+ if (index < 0 || index >= visibleCount) {
return;
}
+ this.ensureChartLineSelectionLength(visibleCount);
const normalized = value || 'none';
if (this.chartLineSelections[index] === normalized) {
return;
@@ -324,10 +424,9 @@ export function refreshChartsForSelections() {
}
export function getActiveChartLineSelections() {
- if (!Array.isArray(this.chartLineSelections)) {
- this.chartLineSelections = ['none', 'none', 'none'];
- }
- return this.chartLineSelections
+ const visibleCount = this.getVisibleChartLineCount();
+ const selections = this.ensureChartLineSelectionLength(visibleCount).slice(0, visibleCount);
+ return selections
.map((value, index) => ({ model: value, index }))
.filter(item => item.model && item.model !== 'none');
}
@@ -763,7 +862,7 @@ export function buildChartDataForMetric(period = 'day', metric = 'requests') {
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];
+ const style = this.chartLineStyles[selection.index % this.chartLineStyles.length] || this.chartLineStyles[0];
return {
label: selection.model,
data: values,
@@ -1361,6 +1460,12 @@ export const usageModule = {
loadUsageStats,
updateUsageOverview,
getModelNamesFromUsage,
+ getChartLineMaxCount,
+ getVisibleChartLineCount,
+ ensureChartLineSelectionLength,
+ updateChartLineControlsUI,
+ setChartLineVisibleCount,
+ changeChartLineCount,
updateChartLineSelectors,
handleChartLineSelectionChange,
refreshChartsForSelections,
diff --git a/styles.css b/styles.css
index fc0cd6b..adb046e 100644
--- a/styles.css
+++ b/styles.css
@@ -3098,12 +3098,41 @@ input:checked+.slider:before {
min-width: 220px;
}
+.usage-filter-actions {
+ min-width: 260px;
+}
+
.usage-filter-group label {
font-weight: 600;
color: var(--text-secondary);
font-size: 14px;
}
+.chart-line-actions {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.chart-line-count {
+ padding: 6px 10px;
+ border-radius: 20px;
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
+ font-size: 13px;
+ border: 1px solid var(--border-color);
+}
+
+.chart-line-hint {
+ color: var(--text-secondary);
+ font-size: 13px;
+}
+
+.chart-line-group.chart-line-hidden {
+ display: none;
+}
+
.model-filter-select {
border: 1px solid var(--border-color);
border-radius: 10px;