mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
Update 0.0.3Beta
This commit is contained in:
363
app.js
363
app.js
@@ -607,6 +607,29 @@ class CLIProxyManager {
|
|||||||
authFileInput.addEventListener('change', (e) => this.handleFileUpload(e));
|
authFileInput.addEventListener('change', (e) => this.handleFileUpload(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用统计
|
||||||
|
const refreshUsageStats = document.getElementById('refresh-usage-stats');
|
||||||
|
const requestsHourBtn = document.getElementById('requests-hour-btn');
|
||||||
|
const requestsDayBtn = document.getElementById('requests-day-btn');
|
||||||
|
const tokensHourBtn = document.getElementById('tokens-hour-btn');
|
||||||
|
const tokensDayBtn = document.getElementById('tokens-day-btn');
|
||||||
|
|
||||||
|
if (refreshUsageStats) {
|
||||||
|
refreshUsageStats.addEventListener('click', () => this.loadUsageStats());
|
||||||
|
}
|
||||||
|
if (requestsHourBtn) {
|
||||||
|
requestsHourBtn.addEventListener('click', () => this.switchRequestsPeriod('hour'));
|
||||||
|
}
|
||||||
|
if (requestsDayBtn) {
|
||||||
|
requestsDayBtn.addEventListener('click', () => this.switchRequestsPeriod('day'));
|
||||||
|
}
|
||||||
|
if (tokensHourBtn) {
|
||||||
|
tokensHourBtn.addEventListener('click', () => this.switchTokensPeriod('hour'));
|
||||||
|
}
|
||||||
|
if (tokensDayBtn) {
|
||||||
|
tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day'));
|
||||||
|
}
|
||||||
|
|
||||||
// 模态框
|
// 模态框
|
||||||
const closeBtn = document.querySelector('.close');
|
const closeBtn = document.querySelector('.close');
|
||||||
if (closeBtn) {
|
if (closeBtn) {
|
||||||
@@ -939,6 +962,9 @@ class CLIProxyManager {
|
|||||||
// 认证文件需要单独加载,因为不在配置中
|
// 认证文件需要单独加载,因为不在配置中
|
||||||
await this.loadAuthFiles();
|
await this.loadAuthFiles();
|
||||||
|
|
||||||
|
// 使用统计需要单独加载
|
||||||
|
await this.loadUsageStats();
|
||||||
|
|
||||||
console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid());
|
console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载配置失败:', error);
|
console.error('加载配置失败:', error);
|
||||||
@@ -2275,6 +2301,343 @@ class CLIProxyManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 使用统计相关方法 =====
|
||||||
|
|
||||||
|
// 初始化图表变量
|
||||||
|
requestsChart = null;
|
||||||
|
tokensChart = null;
|
||||||
|
currentUsageData = null;
|
||||||
|
|
||||||
|
// 加载使用统计
|
||||||
|
async loadUsageStats() {
|
||||||
|
try {
|
||||||
|
const response = await this.makeRequest('/usage');
|
||||||
|
const usage = response?.usage || null;
|
||||||
|
this.currentUsageData = usage;
|
||||||
|
|
||||||
|
if (!usage) {
|
||||||
|
throw new Error('usage payload missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新概览卡片
|
||||||
|
this.updateUsageOverview(usage);
|
||||||
|
|
||||||
|
// 读取当前图表周期
|
||||||
|
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
|
||||||
|
const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
|
||||||
|
const requestsPeriod = requestsHourActive ? 'hour' : 'day';
|
||||||
|
const tokensPeriod = tokensHourActive ? 'hour' : 'day';
|
||||||
|
|
||||||
|
// 初始化图表(使用当前周期)
|
||||||
|
this.initializeRequestsChart(requestsPeriod);
|
||||||
|
this.initializeTokensChart(tokensPeriod);
|
||||||
|
|
||||||
|
// 更新API详细统计表格
|
||||||
|
this.updateApiStatsTable(usage);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载使用统计失败:', error);
|
||||||
|
this.currentUsageData = null;
|
||||||
|
|
||||||
|
// 清空概览数据
|
||||||
|
['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = '-';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空图表
|
||||||
|
if (this.requestsChart) {
|
||||||
|
this.requestsChart.destroy();
|
||||||
|
this.requestsChart = null;
|
||||||
|
}
|
||||||
|
if (this.tokensChart) {
|
||||||
|
this.tokensChart.destroy();
|
||||||
|
this.tokensChart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableElement = document.getElementById('api-stats-table');
|
||||||
|
if (tableElement) {
|
||||||
|
tableElement.innerHTML = `<div class="no-data-message">${i18n.t('usage_stats.loading_error')}: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新使用统计概览
|
||||||
|
updateUsageOverview(data) {
|
||||||
|
const safeData = data || {};
|
||||||
|
document.getElementById('total-requests').textContent = safeData.total_requests ?? 0;
|
||||||
|
document.getElementById('success-requests').textContent = safeData.success_count ?? 0;
|
||||||
|
document.getElementById('failed-requests').textContent = safeData.failure_count ?? 0;
|
||||||
|
document.getElementById('total-tokens').textContent = safeData.total_tokens ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
initializeCharts() {
|
||||||
|
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
|
||||||
|
const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
|
||||||
|
this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day');
|
||||||
|
this.initializeTokensChart(tokensHourActive ? 'hour' : 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化请求趋势图表
|
||||||
|
initializeRequestsChart(period = 'day') {
|
||||||
|
const ctx = document.getElementById('requests-chart');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// 销毁现有图表
|
||||||
|
if (this.requestsChart) {
|
||||||
|
this.requestsChart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = this.getRequestsChartData(period);
|
||||||
|
|
||||||
|
this.requestsChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: data,
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: i18n.t('usage_stats.requests_count')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
line: {
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
},
|
||||||
|
point: {
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
borderWidth: 2,
|
||||||
|
radius: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化Token使用趋势图表
|
||||||
|
initializeTokensChart(period = 'day') {
|
||||||
|
const ctx = document.getElementById('tokens-chart');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// 销毁现有图表
|
||||||
|
if (this.tokensChart) {
|
||||||
|
this.tokensChart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = this.getTokensChartData(period);
|
||||||
|
|
||||||
|
this.tokensChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: data,
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: i18n.t('usage_stats.tokens_count')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
line: {
|
||||||
|
borderColor: '#10b981',
|
||||||
|
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
},
|
||||||
|
point: {
|
||||||
|
backgroundColor: '#10b981',
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
borderWidth: 2,
|
||||||
|
radius: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取请求图表数据
|
||||||
|
getRequestsChartData(period) {
|
||||||
|
if (!this.currentUsageData) {
|
||||||
|
return { labels: [], datasets: [{ data: [] }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataSource, labels, values;
|
||||||
|
|
||||||
|
if (period === 'hour') {
|
||||||
|
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 {
|
||||||
|
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图表数据
|
||||||
|
getTokensChartData(period) {
|
||||||
|
if (!this.currentUsageData) {
|
||||||
|
return { labels: [], datasets: [{ data: [] }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataSource, labels, values;
|
||||||
|
|
||||||
|
if (period === 'hour') {
|
||||||
|
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 {
|
||||||
|
dataSource = this.currentUsageData.tokens_by_day || {};
|
||||||
|
labels = Object.keys(dataSource).sort();
|
||||||
|
values = labels.map(day => dataSource[day] || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
data: values
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换请求图表时间周期
|
||||||
|
switchRequestsPeriod(period) {
|
||||||
|
// 更新按钮状态
|
||||||
|
document.getElementById('requests-hour-btn').classList.toggle('active', period === 'hour');
|
||||||
|
document.getElementById('requests-day-btn').classList.toggle('active', period === 'day');
|
||||||
|
|
||||||
|
// 更新图表数据
|
||||||
|
if (this.requestsChart) {
|
||||||
|
const newData = this.getRequestsChartData(period);
|
||||||
|
this.requestsChart.data = newData;
|
||||||
|
this.requestsChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day');
|
||||||
|
this.requestsChart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换Token图表时间周期
|
||||||
|
switchTokensPeriod(period) {
|
||||||
|
// 更新按钮状态
|
||||||
|
document.getElementById('tokens-hour-btn').classList.toggle('active', period === 'hour');
|
||||||
|
document.getElementById('tokens-day-btn').classList.toggle('active', period === 'day');
|
||||||
|
|
||||||
|
// 更新图表数据
|
||||||
|
if (this.tokensChart) {
|
||||||
|
const newData = this.getTokensChartData(period);
|
||||||
|
this.tokensChart.data = newData;
|
||||||
|
this.tokensChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day');
|
||||||
|
this.tokensChart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新API详细统计表格
|
||||||
|
updateApiStatsTable(data) {
|
||||||
|
const container = document.getElementById('api-stats-table');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const apis = data.apis || {};
|
||||||
|
|
||||||
|
if (Object.keys(apis).length === 0) {
|
||||||
|
container.innerHTML = `<div class="no-data-message">${i18n.t('usage_stats.no_data')}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tableHtml = `
|
||||||
|
<table class="stats-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>${i18n.t('usage_stats.api_endpoint')}</th>
|
||||||
|
<th>${i18n.t('usage_stats.requests_count')}</th>
|
||||||
|
<th>${i18n.t('usage_stats.tokens_count')}</th>
|
||||||
|
<th>${i18n.t('usage_stats.success_rate')}</th>
|
||||||
|
<th>${i18n.t('usage_stats.models')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
Object.entries(apis).forEach(([endpoint, apiData]) => {
|
||||||
|
const totalRequests = apiData.total_requests || 0;
|
||||||
|
const successCount = apiData.success_count ?? null;
|
||||||
|
const successRate = successCount !== null && totalRequests > 0
|
||||||
|
? Math.round((successCount / totalRequests) * 100)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 构建模型详情
|
||||||
|
let modelsHtml = '';
|
||||||
|
if (apiData.models && Object.keys(apiData.models).length > 0) {
|
||||||
|
modelsHtml = '<div class="model-details">';
|
||||||
|
Object.entries(apiData.models).forEach(([modelName, modelData]) => {
|
||||||
|
const modelRequests = modelData.total_requests ?? 0;
|
||||||
|
const modelTokens = modelData.total_tokens ?? 0;
|
||||||
|
modelsHtml += `
|
||||||
|
<div class="model-item">
|
||||||
|
<span class="model-name">${modelName}</span>
|
||||||
|
<span>${modelRequests} 请求 / ${modelTokens} tokens</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
modelsHtml += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
tableHtml += `
|
||||||
|
<tr>
|
||||||
|
<td>${endpoint}</td>
|
||||||
|
<td>${totalRequests}</td>
|
||||||
|
<td>${apiData.total_tokens || 0}</td>
|
||||||
|
<td>${successRate !== null ? successRate + '%' : '-'}</td>
|
||||||
|
<td>${modelsHtml || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tableHtml += '</tbody></table>';
|
||||||
|
container.innerHTML = tableHtml;
|
||||||
|
}
|
||||||
|
|
||||||
showModal() {
|
showModal() {
|
||||||
const modal = document.getElementById('modal');
|
const modal = document.getElementById('modal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
|
|||||||
42
i18n.js
42
i18n.js
@@ -85,6 +85,7 @@ const i18n = {
|
|||||||
'nav.api_keys': 'API 密钥',
|
'nav.api_keys': 'API 密钥',
|
||||||
'nav.ai_providers': 'AI 提供商',
|
'nav.ai_providers': 'AI 提供商',
|
||||||
'nav.auth_files': '认证文件',
|
'nav.auth_files': '认证文件',
|
||||||
|
'nav.usage_stats': '使用统计',
|
||||||
'nav.system_info': '系统信息',
|
'nav.system_info': '系统信息',
|
||||||
|
|
||||||
// 基础设置
|
// 基础设置
|
||||||
@@ -214,6 +215,26 @@ const i18n = {
|
|||||||
'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值',
|
'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值',
|
||||||
'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功',
|
'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功',
|
||||||
|
|
||||||
|
// 使用统计
|
||||||
|
'usage_stats.title': '使用统计',
|
||||||
|
'usage_stats.total_requests': '总请求数',
|
||||||
|
'usage_stats.success_requests': '成功请求',
|
||||||
|
'usage_stats.failed_requests': '失败请求',
|
||||||
|
'usage_stats.total_tokens': '总Token数',
|
||||||
|
'usage_stats.requests_trend': '请求趋势',
|
||||||
|
'usage_stats.tokens_trend': 'Token 使用趋势',
|
||||||
|
'usage_stats.api_details': 'API 详细统计',
|
||||||
|
'usage_stats.by_hour': '按小时',
|
||||||
|
'usage_stats.by_day': '按天',
|
||||||
|
'usage_stats.refresh': '刷新',
|
||||||
|
'usage_stats.no_data': '暂无数据',
|
||||||
|
'usage_stats.loading_error': '加载失败',
|
||||||
|
'usage_stats.api_endpoint': 'API端点',
|
||||||
|
'usage_stats.requests_count': '请求次数',
|
||||||
|
'usage_stats.tokens_count': 'Token数量',
|
||||||
|
'usage_stats.models': '模型统计',
|
||||||
|
'usage_stats.success_rate': '成功率',
|
||||||
|
|
||||||
// 系统信息
|
// 系统信息
|
||||||
'system_info.title': '系统信息',
|
'system_info.title': '系统信息',
|
||||||
'system_info.connection_status_title': '连接状态',
|
'system_info.connection_status_title': '连接状态',
|
||||||
@@ -358,6 +379,7 @@ const i18n = {
|
|||||||
'nav.api_keys': 'API Keys',
|
'nav.api_keys': 'API Keys',
|
||||||
'nav.ai_providers': 'AI Providers',
|
'nav.ai_providers': 'AI Providers',
|
||||||
'nav.auth_files': 'Auth Files',
|
'nav.auth_files': 'Auth Files',
|
||||||
|
'nav.usage_stats': 'Usage Statistics',
|
||||||
'nav.system_info': 'System Info',
|
'nav.system_info': 'System Info',
|
||||||
|
|
||||||
// Basic settings
|
// Basic settings
|
||||||
@@ -487,6 +509,26 @@ const i18n = {
|
|||||||
'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value',
|
'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value',
|
||||||
'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully',
|
'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully',
|
||||||
|
|
||||||
|
// Usage Statistics
|
||||||
|
'usage_stats.title': 'Usage Statistics',
|
||||||
|
'usage_stats.total_requests': 'Total Requests',
|
||||||
|
'usage_stats.success_requests': 'Success Requests',
|
||||||
|
'usage_stats.failed_requests': 'Failed Requests',
|
||||||
|
'usage_stats.total_tokens': 'Total Tokens',
|
||||||
|
'usage_stats.requests_trend': 'Request Trends',
|
||||||
|
'usage_stats.tokens_trend': 'Token Usage Trends',
|
||||||
|
'usage_stats.api_details': 'API Details',
|
||||||
|
'usage_stats.by_hour': 'By Hour',
|
||||||
|
'usage_stats.by_day': 'By Day',
|
||||||
|
'usage_stats.refresh': 'Refresh',
|
||||||
|
'usage_stats.no_data': 'No Data Available',
|
||||||
|
'usage_stats.loading_error': 'Loading Failed',
|
||||||
|
'usage_stats.api_endpoint': 'API Endpoint',
|
||||||
|
'usage_stats.requests_count': 'Request Count',
|
||||||
|
'usage_stats.tokens_count': 'Token Count',
|
||||||
|
'usage_stats.models': 'Model Statistics',
|
||||||
|
'usage_stats.success_rate': 'Success Rate',
|
||||||
|
|
||||||
// System info
|
// System info
|
||||||
'system_info.title': 'System Information',
|
'system_info.title': 'System Information',
|
||||||
'system_info.connection_status_title': 'Connection Status',
|
'system_info.connection_status_title': 'Connection Status',
|
||||||
|
|||||||
110
index.html
110
index.html
@@ -6,6 +6,7 @@
|
|||||||
<title data-i18n="title.login">CLI Proxy API Management Center</title>
|
<title data-i18n="title.login">CLI Proxy API Management Center</title>
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script src="i18n.js"></script>
|
<script src="i18n.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -221,6 +222,9 @@
|
|||||||
<li><a href="#auth-files" class="nav-item" data-section="auth-files">
|
<li><a href="#auth-files" class="nav-item" data-section="auth-files">
|
||||||
<i class="fas fa-file-alt"></i> <span data-i18n="nav.auth_files">认证文件</span>
|
<i class="fas fa-file-alt"></i> <span data-i18n="nav.auth_files">认证文件</span>
|
||||||
</a></li>
|
</a></li>
|
||||||
|
<li><a href="#usage-stats" class="nav-item" data-section="usage-stats">
|
||||||
|
<i class="fas fa-chart-line"></i> <span data-i18n="nav.usage_stats">使用统计</span>
|
||||||
|
</a></li>
|
||||||
<li><a href="#system-info" class="nav-item" data-section="system-info">
|
<li><a href="#system-info" class="nav-item" data-section="system-info">
|
||||||
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
|
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
|
||||||
</a></li>
|
</a></li>
|
||||||
@@ -452,6 +456,112 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 使用统计 -->
|
||||||
|
<section id="usage-stats" class="content-section">
|
||||||
|
<h2 data-i18n="usage_stats.title">使用统计</h2>
|
||||||
|
|
||||||
|
<!-- 概览统计卡片 -->
|
||||||
|
<div class="stats-overview">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-paper-plane"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number" id="total-requests">0</div>
|
||||||
|
<div class="stat-label" data-i18n="usage_stats.total_requests">总请求数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon success">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number" id="success-requests">0</div>
|
||||||
|
<div class="stat-label" data-i18n="usage_stats.success_requests">成功请求</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon error">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number" id="failed-requests">0</div>
|
||||||
|
<div class="stat-label" data-i18n="usage_stats.failed_requests">失败请求</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-coins"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number" id="total-tokens">0</div>
|
||||||
|
<div class="stat-label" data-i18n="usage_stats.total_tokens">总Token数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<div class="charts-container">
|
||||||
|
<!-- 请求趋势图 -->
|
||||||
|
<div class="card chart-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-chart-line"></i> <span data-i18n="usage_stats.requests_trend">请求趋势</span></h3>
|
||||||
|
<div class="chart-controls">
|
||||||
|
<button class="btn btn-small" data-period="hour" id="requests-hour-btn">
|
||||||
|
<span data-i18n="usage_stats.by_hour">按小时</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-small active" data-period="day" id="requests-day-btn">
|
||||||
|
<span data-i18n="usage_stats.by_day">按天</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="requests-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Token使用趋势图 -->
|
||||||
|
<div class="card chart-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-chart-area"></i> <span data-i18n="usage_stats.tokens_trend">Token 使用趋势</span></h3>
|
||||||
|
<div class="chart-controls">
|
||||||
|
<button class="btn btn-small" data-period="hour" id="tokens-hour-btn">
|
||||||
|
<span data-i18n="usage_stats.by_hour">按小时</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-small active" data-period="day" id="tokens-day-btn">
|
||||||
|
<span data-i18n="usage_stats.by_day">按天</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="tokens-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API详细统计 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-list"></i> <span data-i18n="usage_stats.api_details">API 详细统计</span></h3>
|
||||||
|
<button id="refresh-usage-stats" class="btn btn-primary">
|
||||||
|
<i class="fas fa-sync-alt"></i> <span data-i18n="usage_stats.refresh">刷新</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div id="api-stats-table" class="api-stats-table">
|
||||||
|
<div class="loading-placeholder" data-i18n="common.loading">正在加载...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- 系统信息 -->
|
<!-- 系统信息 -->
|
||||||
<section id="system-info" class="content-section">
|
<section id="system-info" class="content-section">
|
||||||
<h2 data-i18n="system_info.title">系统信息</h2>
|
<h2 data-i18n="system_info.title">系统信息</h2>
|
||||||
|
|||||||
203
styles.css
203
styles.css
@@ -27,6 +27,9 @@
|
|||||||
--accent-primary: linear-gradient(135deg, #475569, #334155);
|
--accent-primary: linear-gradient(135deg, #475569, #334155);
|
||||||
--accent-secondary: #e2e8f0;
|
--accent-secondary: #e2e8f0;
|
||||||
--accent-tertiary: #f8fafc;
|
--accent-tertiary: #f8fafc;
|
||||||
|
--primary-color: #3b82f6;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--border-color: #e2e8f0;
|
||||||
|
|
||||||
--success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0);
|
--success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0);
|
||||||
--success-text: #166534;
|
--success-text: #166534;
|
||||||
@@ -69,6 +72,9 @@
|
|||||||
--accent-primary: linear-gradient(135deg, #64748b, #475569);
|
--accent-primary: linear-gradient(135deg, #64748b, #475569);
|
||||||
--accent-secondary: #334155;
|
--accent-secondary: #334155;
|
||||||
--accent-tertiary: #1e293b;
|
--accent-tertiary: #1e293b;
|
||||||
|
--primary-color: #38bdf8;
|
||||||
|
--card-bg: #1e293b;
|
||||||
|
--border-color: #334155;
|
||||||
|
|
||||||
--success-bg: linear-gradient(135deg, #064e3b, #047857);
|
--success-bg: linear-gradient(135deg, #064e3b, #047857);
|
||||||
--success-text: #bbf7d0;
|
--success-text: #bbf7d0;
|
||||||
@@ -1613,3 +1619,200 @@ input:checked + .slider:before {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 使用统计样式 */
|
||||||
|
.stats-overview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.success {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.error {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.charts-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-small {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-small:hover {
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-small.active {
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-stats-table {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table th,
|
||||||
|
.stats-table td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table th {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table td {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table tr:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-details {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-message {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题适配 */
|
||||||
|
[data-theme="dark"] .stat-card {
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .stat-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .btn.btn-small {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user