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;