mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +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));
|
||||
}
|
||||
|
||||
// 使用统计
|
||||
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');
|
||||
if (closeBtn) {
|
||||
@@ -939,6 +962,9 @@ class CLIProxyManager {
|
||||
// 认证文件需要单独加载,因为不在配置中
|
||||
await this.loadAuthFiles();
|
||||
|
||||
// 使用统计需要单独加载
|
||||
await this.loadUsageStats();
|
||||
|
||||
console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid());
|
||||
} catch (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() {
|
||||
const modal = document.getElementById('modal');
|
||||
if (modal) {
|
||||
|
||||
42
i18n.js
42
i18n.js
@@ -85,6 +85,7 @@ const i18n = {
|
||||
'nav.api_keys': 'API 密钥',
|
||||
'nav.ai_providers': 'AI 提供商',
|
||||
'nav.auth_files': '认证文件',
|
||||
'nav.usage_stats': '使用统计',
|
||||
'nav.system_info': '系统信息',
|
||||
|
||||
// 基础设置
|
||||
@@ -214,6 +215,26 @@ const i18n = {
|
||||
'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值',
|
||||
'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.connection_status_title': '连接状态',
|
||||
@@ -358,6 +379,7 @@ const i18n = {
|
||||
'nav.api_keys': 'API Keys',
|
||||
'nav.ai_providers': 'AI Providers',
|
||||
'nav.auth_files': 'Auth Files',
|
||||
'nav.usage_stats': 'Usage Statistics',
|
||||
'nav.system_info': 'System Info',
|
||||
|
||||
// Basic settings
|
||||
@@ -487,6 +509,26 @@ const i18n = {
|
||||
'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value',
|
||||
'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.title': 'System Information',
|
||||
'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>
|
||||
<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">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="i18n.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -221,6 +222,9 @@
|
||||
<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>
|
||||
</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">
|
||||
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
|
||||
</a></li>
|
||||
@@ -452,6 +456,112 @@
|
||||
</div>
|
||||
</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">
|
||||
<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-secondary: #e2e8f0;
|
||||
--accent-tertiary: #f8fafc;
|
||||
--primary-color: #3b82f6;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #e2e8f0;
|
||||
|
||||
--success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0);
|
||||
--success-text: #166534;
|
||||
@@ -69,6 +72,9 @@
|
||||
--accent-primary: linear-gradient(135deg, #64748b, #475569);
|
||||
--accent-secondary: #334155;
|
||||
--accent-tertiary: #1e293b;
|
||||
--primary-color: #38bdf8;
|
||||
--card-bg: #1e293b;
|
||||
--border-color: #334155;
|
||||
|
||||
--success-bg: linear-gradient(135deg, #064e3b, #047857);
|
||||
--success-text: #bbf7d0;
|
||||
@@ -1613,3 +1619,200 @@ input:checked + .slider:before {
|
||||
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