From 4d419448e81f787c6f44c08f758a31c7e67634a2 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Thu, 4 Dec 2025 00:55:24 +0800 Subject: [PATCH] feat: implement chart line deletion functionality with UI controls and internationalization support --- app.js | 14 ++++-- i18n.js | 6 ++- index.html | 112 +++++++++++++++++++++++++++++++------------ src/modules/usage.js | 108 +++++++++++++++++++++++++++++++++-------- styles.css | 14 ++++++ 5 files changed, 197 insertions(+), 57 deletions(-) diff --git a/app.js b/app.js index 1e187d6..cb46852 100644 --- a/app.js +++ b/app.js @@ -525,8 +525,8 @@ class CLIProxyManager { 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 chartLineDeleteButtons = document.querySelectorAll('.chart-line-delete'); const modelPriceForm = document.getElementById('model-price-form'); const resetModelPricesBtn = document.getElementById('reset-model-prices'); const modelPriceSelect = document.getElementById('model-price-model-select'); @@ -555,9 +555,6 @@ class CLIProxyManager { 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) => { @@ -566,6 +563,14 @@ class CLIProxyManager { }); }); } + if (chartLineDeleteButtons.length) { + chartLineDeleteButtons.forEach(button => { + button.addEventListener('click', () => { + const index = Number.parseInt(button.getAttribute('data-line-index'), 10); + this.removeChartLine(Number.isNaN(index) ? -1 : index); + }); + }); + } this.updateChartLineControlsUI(); if (modelPriceForm) { modelPriceForm.addEventListener('submit', (event) => { @@ -673,6 +678,7 @@ class CLIProxyManager { chartLineMaxCount = 9; chartLineVisibleCount = 3; chartLineSelections = Array(3).fill('none'); + chartLineSelectionsInitialized = false; chartLineSelectIds = Array.from({ length: 9 }, (_, idx) => `chart-line-select-${idx}`); chartLineStyles = [ { borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.15)' }, diff --git a/i18n.js b/i18n.js index a32e1fd..5f8be4f 100644 --- a/i18n.js +++ b/i18n.js @@ -472,7 +472,8 @@ const i18n = { '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_all': '全部', + 'usage_stats.chart_line_delete': '删除曲线', 'usage_stats.chart_line_hint': '最多同时显示 9 条模型曲线', 'usage_stats.no_data': '暂无数据', 'usage_stats.loading_error': '加载失败', @@ -1103,7 +1104,8 @@ const i18n = { '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_all': 'All', + 'usage_stats.chart_line_delete': 'Delete 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', diff --git a/index.html b/index.html index 4f78cad..3963679 100644 --- a/index.html +++ b/index.html @@ -966,67 +966,117 @@ 增加曲线 - 3/9
最多显示 9 条模型曲线
- +
+ + +
- +
+ + +
- +
+ + +
- +
+ + +
- +
+ + +
- +
+ + +
- +
+ + +
- +
+ + +
- +
+ + +
diff --git a/src/modules/usage.js b/src/modules/usage.js index 1380e79..731c2bb 100644 --- a/src/modules/usage.js +++ b/src/modules/usage.js @@ -3,6 +3,7 @@ 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; +const ALL_MODELS_VALUE = 'all'; // 获取API密钥的统计信息 export async function getKeyStats(usageData = null) { @@ -269,12 +270,19 @@ export function updateChartLineControlsUI() { 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; + const deleteButtons = document.querySelectorAll('.chart-line-delete'); + if (deleteButtons.length) { + deleteButtons.forEach(button => { + const group = button.closest('.chart-line-group'); + const index = Number.parseInt(button.getAttribute('data-line-index'), 10); + const isVisible = group + ? !group.classList.contains('chart-line-hidden') + : (Number.isFinite(index) ? index < visibleCount : true); + button.disabled = visibleCount <= MIN_CHART_LINE_COUNT || !isVisible; + }); } } @@ -297,6 +305,23 @@ export function changeChartLineCount(delta = 0) { this.setChartLineVisibleCount(current + delta); } +export function removeChartLine(index) { + const visibleCount = this.getVisibleChartLineCount(); + const normalizedIndex = Number.parseInt(index, 10); + if (!Number.isFinite(normalizedIndex) || normalizedIndex < 0 || normalizedIndex >= visibleCount) { + return; + } + if (visibleCount <= MIN_CHART_LINE_COUNT) { + return; + } + const nextSelections = this.ensureChartLineSelectionLength(visibleCount).slice(0, visibleCount); + nextSelections.splice(normalizedIndex, 1); + this.chartLineSelections = nextSelections; + this.chartLineVisibleCount = Math.max(MIN_CHART_LINE_COUNT, visibleCount - 1); + this.updateChartLineSelectors(this.currentUsageData); + this.refreshChartsForSelections(); +} + export function updateChartLineSelectors(usage) { const modelNames = this.getModelNamesFromUsage(usage); const selectors = this.chartLineSelectIds @@ -307,19 +332,21 @@ export function updateChartLineSelectors(usage) { const visibleCount = Math.min(this.getVisibleChartLineCount(), availableCount); this.chartLineVisibleCount = visibleCount; this.ensureChartLineSelectionLength(visibleCount); + const wasInitialized = this.chartLineSelectionsInitialized === true; if (!selectors.length) { this.chartLineSelections = Array(visibleCount).fill('none'); + this.chartLineSelectionsInitialized = false; this.updateChartLineControlsUI(); return; } const optionsFragment = () => { const fragment = document.createDocumentFragment(); - const hiddenOption = document.createElement('option'); - hiddenOption.value = 'none'; - hiddenOption.textContent = i18n.t('usage_stats.chart_line_hidden'); - fragment.appendChild(hiddenOption); + const allOption = document.createElement('option'); + allOption.value = ALL_MODELS_VALUE; + allOption.textContent = i18n.t('usage_stats.chart_line_all'); + fragment.appendChild(allOption); modelNames.forEach(name => { const option = document.createElement('option'); option.value = name; @@ -336,22 +363,27 @@ export function updateChartLineSelectors(usage) { if (group) { group.classList.toggle('chart-line-hidden', !isVisible); } + const deleteBtn = group ? group.querySelector('.chart-line-delete') : null; select.innerHTML = ''; select.appendChild(optionsFragment()); - select.disabled = !hasModels || !isVisible; + select.disabled = !isVisible; + if (deleteBtn) { + deleteBtn.disabled = !isVisible || visibleCount <= MIN_CHART_LINE_COUNT; + } if (!isVisible) { - select.value = 'none'; + select.value = ALL_MODELS_VALUE; } }); if (!hasModels) { - this.chartLineSelections = Array(visibleCount).fill('none'); + this.chartLineSelections = Array(visibleCount).fill(ALL_MODELS_VALUE); + this.chartLineSelectionsInitialized = false; selectors.forEach((select, index) => { const group = select.closest('.chart-line-group'); if (group) { group.classList.toggle('chart-line-hidden', index >= visibleCount); } - select.value = 'none'; + select.value = ALL_MODELS_VALUE; }); this.updateChartLineControlsUI(); return; @@ -359,29 +391,38 @@ export function updateChartLineSelectors(usage) { const nextSelections = this.ensureChartLineSelectionLength(visibleCount).slice(0, visibleCount); - const validNames = new Set(modelNames); + const validNames = new Set([...modelNames, ALL_MODELS_VALUE]); 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'; + nextSelections[i] = ALL_MODELS_VALUE; } - if (nextSelections[i] !== 'none') { + if (nextSelections[i] && nextSelections[i] !== 'none') { hasActiveSelection = true; } } - if (!hasActiveSelection) { + const allSelectionsAreAll = nextSelections.length > 0 && nextSelections.every(value => value === ALL_MODELS_VALUE); + + if (!hasActiveSelection || (!wasInitialized && allSelectionsAreAll)) { modelNames.slice(0, nextSelections.length).forEach((name, index) => { nextSelections[index] = name; }); } + for (let i = 0; i < nextSelections.length; i++) { + if (!nextSelections[i] || nextSelections[i] === 'none') { + nextSelections[i] = modelNames[i % Math.max(modelNames.length, 1)] || ALL_MODELS_VALUE; + } + } + this.chartLineSelections = nextSelections; selectors.forEach((select, index) => { - const value = this.chartLineSelections[index] || 'none'; - select.value = index < visibleCount ? value : 'none'; + const value = this.chartLineSelections[index] || ALL_MODELS_VALUE; + select.value = index < visibleCount ? value : ALL_MODELS_VALUE; }); + this.chartLineSelectionsInitialized = hasModels; this.updateChartLineControlsUI(); } @@ -391,7 +432,7 @@ export function handleChartLineSelectionChange(index, value) { return; } this.ensureChartLineSelectionLength(visibleCount); - const normalized = value || 'none'; + const normalized = (value && value !== 'none') ? value : ALL_MODELS_VALUE; if (this.chartLineSelections[index] === normalized) { return; } @@ -860,11 +901,37 @@ export function buildChartDataForMetric(period = 'day', metric = 'requests') { const labels = baseSeries?.labels || []; const dataByModel = baseSeries?.dataByModel || new Map(); const activeSelections = this.getActiveChartLineSelections(); + let allSeriesCache = null; + + const getAllSeries = () => { + if (allSeriesCache) { + return allSeriesCache; + } + const summed = new Array(labels.length).fill(0); + dataByModel.forEach(values => { + values.forEach((value, idx) => { + summed[idx] = (summed[idx] || 0) + value; + }); + }); + allSeriesCache = summed; + return summed; + }; + + const getSeriesForSelection = (selectionValue) => { + if (selectionValue === ALL_MODELS_VALUE) { + return getAllSeries(); + } + return dataByModel.get(selectionValue) || new Array(labels.length).fill(0); + }; + const datasets = activeSelections.map(selection => { - const values = dataByModel.get(selection.model) || new Array(labels.length).fill(0); + const values = getSeriesForSelection(selection.model); const style = this.chartLineStyles[selection.index % this.chartLineStyles.length] || this.chartLineStyles[0]; + const label = selection.model === ALL_MODELS_VALUE + ? i18n.t('usage_stats.chart_line_all') + : selection.model; return { - label: selection.model, + label, data: values, borderColor: style.borderColor, backgroundColor: style.backgroundColor, @@ -1466,6 +1533,7 @@ export const usageModule = { updateChartLineControlsUI, setChartLineVisibleCount, changeChartLineCount, + removeChartLine, updateChartLineSelectors, handleChartLineSelectionChange, refreshChartsForSelections, diff --git a/styles.css b/styles.css index 5a526f4..553eaed 100644 --- a/styles.css +++ b/styles.css @@ -3128,6 +3128,20 @@ input:checked+.slider:before { font-size: 13px; } +.chart-line-control { + display: flex; + align-items: center; + gap: 8px; +} + +.chart-line-control .model-filter-select { + flex: 1; +} + +.chart-line-delete { + white-space: nowrap; +} + .chart-line-group.chart-line-hidden { display: none; }