update ui

This commit is contained in:
Von committed 2026-03-19 23:39:51 +08:00
1 parent ae8a87e844
commit 806691dba4
7 files changed
+333 -86

No files matched your search

+9 -3
View File
@@ -8,6 +8,9 @@
--surface: #ffffff;
--surface-soft: #f8fbff;
--surface-hover: #f2f7fd;
--nav-tab-hover-bg: #edf4fc;
--nav-tab-hover-border: #c6d9ec;
--nav-tab-hover-text: #4575a4;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #64748b;
@@ -34,6 +37,9 @@
--surface: #161b22;
--surface-soft: #0f141b;
--surface-hover: #1f2630;
--nav-tab-hover-bg: #18304b;
--nav-tab-hover-border: #2f5b8f;
--nav-tab-hover-text: #dbeafe;
--text-primary: #e6edf3;
--text-secondary: #9da7b3;
--text-muted: #7d8590;
@@ -204,9 +210,9 @@ body.app-page {
}
.app-page .header-nav-link:hover {
color: var(--text-primary);
background: var(--surface-hover);
border-color: var(--line);
color: var(--nav-tab-hover-text);
background: var(--nav-tab-hover-bg);
border-color: var(--nav-tab-hover-border);
}
.app-page .header-nav-link.active {
+118 -10
View File
@@ -1,3 +1,7 @@
.dashboard-page {
--dashboard-control-height: 40px;
}
.dashboard-page .header-content {
display: flex;
justify-content: space-between;
@@ -10,6 +14,7 @@
}
.dashboard-page .filters-section,
.dashboard-page .dashboard-tabs-section,
.dashboard-page .stats-section,
.dashboard-page .table-section {
padding: 22px var(--page-card-inset);
@@ -40,6 +45,81 @@
.dashboard-page .filters .btn {
width: 110px;
min-height: var(--dashboard-control-height);
height: var(--dashboard-control-height);
display: inline-flex;
align-items: center;
justify-content: center;
}
.dashboard-page .dashboard-tabs-section {
padding: 18px var(--page-card-inset) 0;
background: transparent;
}
.dashboard-page .dashboard-tabs {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px;
margin-bottom: 18px;
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(255, 255, 255, 0.9);
box-shadow: var(--shadow-card);
}
.dashboard-page .dashboard-tab-btn {
min-width: 104px;
height: 36px;
padding: 0 16px;
border: 1px solid transparent;
border-radius: 12px;
background: transparent;
color: var(--text-secondary);
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: background-color var(--transition), color var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.dashboard-page .dashboard-tab-btn:hover,
.dashboard-page .dashboard-tab-btn:focus-visible {
color: var(--nav-tab-hover-text);
background: var(--nav-tab-hover-bg);
border-color: var(--nav-tab-hover-border);
outline: none;
}
.dashboard-page .dashboard-tab-btn.is-active {
color: #ffffff;
background: linear-gradient(180deg, #3a7fed, #2f6ed4);
box-shadow: 0 10px 20px rgba(58, 127, 237, 0.24);
}
.dashboard-page .dashboard-tab-panel[hidden] {
display: none !important;
}
:root[data-theme="dark"] .dashboard-page .dashboard-tabs {
border-color: #30363d;
background: rgba(22, 27, 34, 0.94);
}
:root[data-theme="dark"] .dashboard-page .dashboard-tab-btn {
color: #9da7b3;
}
:root[data-theme="dark"] .dashboard-page .dashboard-tab-btn:hover,
:root[data-theme="dark"] .dashboard-page .dashboard-tab-btn:focus-visible {
color: var(--nav-tab-hover-text);
background: var(--nav-tab-hover-bg);
}
:root[data-theme="dark"] .dashboard-page .dashboard-tab-btn.is-active {
color: #f8fbff;
background: linear-gradient(180deg, #2f81f7, #1f6feb);
box-shadow: 0 10px 22px rgba(31, 111, 235, 0.28);
}
.dashboard-page .filter-group {
@@ -80,29 +160,33 @@
display: flex;
align-items: center;
gap: 8px;
min-height: 40px;
min-height: var(--dashboard-control-height);
flex-wrap: wrap;
}
.dashboard-page .date-preset-btn {
min-width: 68px;
height: 32px;
padding: 0 12px;
min-height: var(--dashboard-control-height);
height: var(--dashboard-control-height);
padding: 0 14px;
border: 1px solid var(--line-strong);
border-radius: 999px;
background: rgba(255, 255, 255, 0.88);
color: var(--text-secondary);
font-size: 12px;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color var(--transition), background-color var(--transition), color var(--transition), box-shadow var(--transition);
}
.dashboard-page .date-preset-btn:hover,
.dashboard-page .date-preset-btn:focus-visible {
border-color: var(--primary);
background: #ffffff;
color: var(--primary);
border-color: var(--nav-tab-hover-border);
background: var(--nav-tab-hover-bg);
color: var(--nav-tab-hover-text);
outline: none;
}
@@ -116,16 +200,28 @@
.dashboard-page .filter-group input,
.dashboard-page .filter-group select {
width: 100%;
height: 40px;
min-height: var(--dashboard-control-height);
height: var(--dashboard-control-height);
border: 1px solid var(--line-strong);
border-radius: var(--radius-sm);
background: #fff;
color: var(--text-primary);
padding: 0 12px;
font-size: 14px;
line-height: 1.2;
transition: border-color var(--transition), box-shadow var(--transition), background-color var(--transition);
}
.dashboard-page .custom-select-trigger {
min-height: var(--dashboard-control-height);
height: var(--dashboard-control-height);
padding: 0 38px 0 12px;
border-radius: var(--radius-sm);
font-size: 14px;
display: inline-flex;
align-items: center;
}
.dashboard-page .filter-group select {
appearance: none;
-webkit-appearance: none;
@@ -173,9 +269,9 @@
:root[data-theme="dark"] .dashboard-page .date-preset-btn:hover,
:root[data-theme="dark"] .dashboard-page .date-preset-btn:focus-visible {
border-color: #5f8fbe;
background: #111820;
color: #c7e0ff;
border-color: var(--nav-tab-hover-border);
background: var(--nav-tab-hover-bg);
color: var(--nav-tab-hover-text);
}
:root[data-theme="dark"] .dashboard-page .date-preset-btn.is-active {
@@ -447,6 +543,7 @@
@media (max-width: 900px) {
.dashboard-page .header,
.dashboard-page .filters-section,
.dashboard-page .dashboard-tabs-section,
.dashboard-page .stats-section,
.dashboard-page .table-section {
padding-left: 16px;
@@ -461,12 +558,23 @@
@media (max-width: 640px) {
.dashboard-page .filters-section,
.dashboard-page .dashboard-tabs-section,
.dashboard-page .stats-section,
.dashboard-page .table-section {
padding-left: 12px;
padding-right: 12px;
}
.dashboard-page .dashboard-tabs {
display: flex;
width: 100%;
}
.dashboard-page .dashboard-tab-btn {
flex: 1 1 0;
min-width: 0;
}
.dashboard-page .filter-group {
width: 100%;
min-width: 0;
+8 -2
View File
@@ -232,7 +232,8 @@
background: var(--surface-hover);
}
.providers-page .provider-name-cell {
.providers-page .providers-table td.provider-name-cell {
vertical-align: middle;
color: var(--text-primary);
white-space: nowrap;
}
@@ -274,12 +275,17 @@
color: #1d6b46;
}
.providers-page .provider-api-cell {
.providers-page .providers-table td.provider-api-cell {
width: 520px;
max-width: 520px;
vertical-align: middle;
color: var(--text-secondary);
}
.providers-page .providers-table td.provider-model-cell {
vertical-align: middle;
}
.providers-page .provider-api-text {
display: -webkit-box;
-webkit-line-clamp: 2;
+1 -1
View File
@@ -6,7 +6,7 @@
{% include "_theme_bootstrap.html" %}
<title>{% block page_title %}LLM Proxy{% endblock %}</title>
<link rel="stylesheet" href="/static/vendor/bootstrap/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/admin-base.css?v=20260319-1">
<link rel="stylesheet" href="/static/css/admin-base.css?v=20260319-3">
{% block page_css %}{% endblock %}
</head>
<body class="{% block body_class_prefix %}{% endblock %}{% block body_class %}{% endblock %}">
+146 -68
View File
@@ -1,7 +1,7 @@
{% extends "base_admin.html" %}
{% block page_title %}LLM使用统计{% endblock %}
{% block page_css %}<link rel="stylesheet" href="/static/css/index.css?v=20260319-1">{% endblock %}
{% block page_css %}<link rel="stylesheet" href="/static/css/index.css?v=20260319-4">{% endblock %}
{% block body_class %}dashboard-page{% endblock %}
{% block header_title %}LLM使用统计{% endblock %}
{% block header_subtitle %}实时监控 API 请求和 Token 消耗{% endblock %}
@@ -41,75 +41,96 @@
</div>
</div>
<div class="stats-section">
<div class="stats-container">
<table class="stats-table">
<thead>
<tr>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('ip_address', 'string')">IP <span class="sort-indicator" id="statsSortIndicator_ip_address"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('username', 'string')">用户名 <span class="sort-indicator" id="statsSortIndicator_username"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('request_model', 'string')">请求模型 <span class="sort-indicator" id="statsSortIndicator_request_model"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('response_model', 'string')">响应模型 <span class="sort-indicator" id="statsSortIndicator_response_model"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('prompt_tokens', 'number')">输入token <span class="sort-indicator" id="statsSortIndicator_prompt_tokens"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('completion_tokens', 'number')">输出token <span class="sort-indicator" id="statsSortIndicator_completion_tokens"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('total_tokens', 'number')">总 Token <span class="sort-indicator" id="statsSortIndicator_total_tokens"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('request_count', 'number')">请求数 <span class="sort-indicator" id="statsSortIndicator_request_count"></span></button></th>
</tr>
</thead>
<tbody id="statsTable">
<tr>
<td colspan="8" class="loading">加载统计数据...</td>
</tr>
</tbody>
</table>
<div class="dashboard-tabs-section">
<div class="dashboard-tabs" role="tablist" aria-label="统计视图">
<button
type="button"
class="dashboard-tab-btn is-active"
id="dashboardTabBtn_stats"
onclick="switchDashboardTab('stats')"
aria-selected="true"
aria-controls="dashboardTabPanel_stats"
>统计汇总</button>
<button
type="button"
class="dashboard-tab-btn"
id="dashboardTabBtn_logs"
onclick="switchDashboardTab('logs')"
aria-selected="false"
aria-controls="dashboardTabPanel_logs"
>请求明细</button>
</div>
</div>
<div class="table-section">
<div class="table-card-shell">
<div class="table-container">
<table>
<div class="dashboard-tab-panel is-active" id="dashboardTabPanel_stats" role="tabpanel" aria-labelledby="dashboardTabBtn_stats">
<div class="stats-container">
<table class="stats-table">
<thead>
<tr>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('ip_address', 'string')">IP <span class="sort-indicator" id="logsSortIndicator_ip_address"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('username', 'string')">用户名 <span class="sort-indicator" id="logsSortIndicator_username"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('request_model', 'string')">请求模型 <span class="sort-indicator" id="logsSortIndicator_request_model"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('response_model', 'string')">响应模型 <span class="sort-indicator" id="logsSortIndicator_response_model"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('prompt_tokens', 'number')">输入token <span class="sort-indicator" id="logsSortIndicator_prompt_tokens"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('completion_tokens', 'number')">输出token <span class="sort-indicator" id="logsSortIndicator_completion_tokens"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('total_tokens', 'number')">总 Token <span class="sort-indicator" id="logsSortIndicator_total_tokens"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('start_time', 'date')">开始时间 <span class="sort-indicator" id="logsSortIndicator_start_time"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('end_time', 'date')">结束时间 <span class="sort-indicator" id="logsSortIndicator_end_time"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('duration', 'number')">耗时(秒) <span class="sort-indicator" id="logsSortIndicator_duration"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('ip_address', 'string')">IP <span class="sort-indicator" id="statsSortIndicator_ip_address"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('username', 'string')">用户名 <span class="sort-indicator" id="statsSortIndicator_username"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('request_model', 'string')">请求模型 <span class="sort-indicator" id="statsSortIndicator_request_model"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('response_model', 'string')">响应模型 <span class="sort-indicator" id="statsSortIndicator_response_model"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('prompt_tokens', 'number')">输入token <span class="sort-indicator" id="statsSortIndicator_prompt_tokens"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('completion_tokens', 'number')">输出token <span class="sort-indicator" id="statsSortIndicator_completion_tokens"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('total_tokens', 'number')">总 Token <span class="sort-indicator" id="statsSortIndicator_total_tokens"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleStatsSort('request_count', 'number')">请求数 <span class="sort-indicator" id="statsSortIndicator_request_count"></span></button></th>
</tr>
</thead>
<tbody id="logsTable">
<tbody id="statsTable">
<tr>
<td colspan="10" class="loading">加载请求数据...</td>
<td colspan="8" class="loading">加载统计数据...</td>
</tr>
</tbody>
</table>
</div>
<div class="table-footer" id="tableFooter" style="display: none;">
<div class="right-controls">
<div class="pagination-controls">
<span id="prevBtn" class="page-nav-text disabled" onclick="prevPage()">&lt;</span>
<span class="page-info" id="pageInfo">第 1 页 / 共 1 页</span>
<span id="nextBtn" class="page-nav-text disabled" onclick="nextPage()">&gt;</span>
</div>
<div class="dashboard-tab-panel" id="dashboardTabPanel_logs" role="tabpanel" aria-labelledby="dashboardTabBtn_logs" hidden>
<div class="table-card-shell">
<div class="table-container">
<table>
<thead>
<tr>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('ip_address', 'string')">IP <span class="sort-indicator" id="logsSortIndicator_ip_address"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('username', 'string')">用户名 <span class="sort-indicator" id="logsSortIndicator_username"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('request_model', 'string')">请求模型 <span class="sort-indicator" id="logsSortIndicator_request_model"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('response_model', 'string')">响应模型 <span class="sort-indicator" id="logsSortIndicator_response_model"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('prompt_tokens', 'number')">输入token <span class="sort-indicator" id="logsSortIndicator_prompt_tokens"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('completion_tokens', 'number')">输出token <span class="sort-indicator" id="logsSortIndicator_completion_tokens"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('total_tokens', 'number')">总 Token <span class="sort-indicator" id="logsSortIndicator_total_tokens"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('start_time', 'date')">开始时间 <span class="sort-indicator" id="logsSortIndicator_start_time"></span></button></th>
<th class="sortable-th" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('end_time', 'date')">结束时间 <span class="sort-indicator" id="logsSortIndicator_end_time"></span></button></th>
<th class="sortable-th num-col" aria-sort="none"><button type="button" class="sort-button" onclick="toggleLogsSort('duration', 'number')">耗时(秒) <span class="sort-indicator" id="logsSortIndicator_duration"></span></button></th>
</tr>
</thead>
<tbody id="logsTable">
<tr>
<td colspan="10" class="loading">加载请求数据...</td>
</tr>
</tbody>
</table>
</div>
<div class="table-footer" id="tableFooter" style="display: none;">
<div class="right-controls">
<div class="pagination-controls">
<span id="prevBtn" class="page-nav-text disabled" onclick="prevPage()">&lt;</span>
<span class="page-info" id="pageInfo">第 1 页 / 共 1 页</span>
<span id="nextBtn" class="page-nav-text disabled" onclick="nextPage()">&gt;</span>
</div>
<div class="quick-jump">
<label for="pageJumpInput">跳至</label>
<input id="pageJumpInput" type="number" min="1" value="1">
</div>
<div class="page-size-controls">
<label for="pageSizeSelect">每页显示:</label>
<select id="pageSizeSelect" onchange="changePageSize()">
<option value="50">50</option>
<option value="100">100</option>
<option value="200">200</option>
</select>
</div>
<span class="total-count" id="totalCount">共 0 条</span>
</div>
<div class="quick-jump">
<label for="pageJumpInput">跳至</label>
<input id="pageJumpInput" type="number" min="1" value="1">
</div>
<div class="page-size-controls">
<label for="pageSizeSelect">每页显示:</label>
<select id="pageSizeSelect" onchange="changePageSize()">
<option value="50">50</option>
<option value="100">100</option>
<option value="200">200</option>
</select>
</div>
<span class="total-count" id="totalCount">共 0 条</span>
</div>
</div>
</div>
@@ -127,6 +148,9 @@
let activeCustomSelect = null;
let statsData = [];
let logsData = [];
let activeDashboardTab = 'stats';
let statsRequestToken = 0;
let logsRequestToken = 0;
const statsSortState = {
key: 'total_tokens',
direction: 'desc',
@@ -138,6 +162,50 @@
type: 'date'
};
function getDashboardTabButton(tabName) {
return document.getElementById(`dashboardTabBtn_${tabName}`);
}
function getDashboardTabPanel(tabName) {
return document.getElementById(`dashboardTabPanel_${tabName}`);
}
function syncDashboardTabUi() {
['stats', 'logs'].forEach(tabName => {
const isActive = tabName === activeDashboardTab;
const button = getDashboardTabButton(tabName);
const panel = getDashboardTabPanel(tabName);
if (button) {
button.classList.toggle('is-active', isActive);
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
}
if (panel) {
panel.classList.toggle('is-active', isActive);
panel.hidden = !isActive;
}
});
}
function loadActiveDashboardTabData() {
if (activeDashboardTab === 'stats') {
loadStatistics();
return;
}
loadLogs();
}
function switchDashboardTab(tabName) {
if (!['stats', 'logs'].includes(tabName)) {
return;
}
activeDashboardTab = tabName;
syncDashboardTabUi();
loadActiveDashboardTabData();
}
function closeAllCustomSelects() {
document.querySelectorAll('.custom-select.open').forEach(select => {
select.classList.remove('open');
@@ -295,8 +363,8 @@
});
const table = prefix === 'stats'
? document.querySelector('.stats-table')
: document.querySelector('.table-section table');
? document.querySelector('#dashboardTabPanel_stats table')
: document.querySelector('#dashboardTabPanel_logs table');
if (!table) return;
table.querySelectorAll('th.sortable-th').forEach(th => {
@@ -461,12 +529,12 @@
setupCustomSelect('requestModel');
setupCustomSelect('pageSizeSelect');
setDefaultDateRange();
loadStatistics();
loadUsernames();
loadRequestModels();
loadLogs();
updateSortIndicators('stats', statsSortState, ['ip_address', 'username', 'request_model', 'response_model', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'request_count']);
updateSortIndicators('logs', logsSortState, ['ip_address', 'username', 'request_model', 'response_model', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'start_time', 'end_time', 'duration']);
syncDashboardTabUi();
switchDashboardTab(activeDashboardTab);
// 设置默认每页大小
document.getElementById('pageSizeSelect').value = pageSize;
@@ -506,14 +574,20 @@
// 加载统计数据
async function loadStatistics() {
const requestToken = ++statsRequestToken;
document.getElementById('statsTable').innerHTML = '<tr><td colspan="8" class="loading">鍔犺浇缁熻鏁版嵁...</td></tr>';
try {
const params = new URLSearchParams(currentFilters);
const response = await fetch(`/api/statistics?${params}`);
statsData = await response.json();
const response = await fetch(`/api/statistics?${params}`, { cache: 'no-store' });
const data = await response.json();
if (requestToken !== statsRequestToken) return;
statsData = data;
if (!Array.isArray(statsData)) statsData = [];
applyStatsSort();
renderStatistics();
} catch (error) {
if (requestToken !== statsRequestToken) return;
console.error('加载统计数据失败:', error);
document.getElementById('statsTable').innerHTML = '<tr><td colspan="8" class="error">加载统计数据失败</td></tr>';
showMessage('加载统计数据失败,请稍后重试', 'error');
@@ -613,6 +687,8 @@
// 加载日志数据
async function loadLogs() {
const requestToken = ++logsRequestToken;
document.getElementById('tableFooter').style.display = 'none';
// 显示加载状态
document.getElementById('logsTable').innerHTML = '<tr><td colspan="10" class="loading">加载请求数据...</td></tr>';
@@ -623,14 +699,17 @@
...currentFilters
});
const response = await fetch(`/api/request-logs?${params}`);
const response = await fetch(`/api/request-logs?${params}`, { cache: 'no-store' });
const data = await response.json();
if (requestToken !== logsRequestToken) return;
logsData = Array.isArray(data.logs) ? data.logs : [];
applyLogsSort();
renderLogs(logsData);
updatePagination(data);
} catch (error) {
if (requestToken !== logsRequestToken) return;
document.getElementById('tableFooter').style.display = 'none';
console.error('加载日志失败:', error);
document.getElementById('logsTable').innerHTML = '<tr><td colspan="10" class="error">加载日志失败</td></tr>';
showMessage('加载日志失败,请稍后重试', 'error');
@@ -752,8 +831,7 @@
if (requestModel) currentFilters.request_model = requestModel;
currentPage = 1;
loadStatistics();
loadLogs();
loadActiveDashboardTabData();
}
function setDefaultDateRange() {
@@ -786,7 +864,7 @@
const newPageSize = parseInt(document.getElementById('pageSizeSelect').value);
if (newPageSize !== pageSize) {
pageSize = newPageSize;
currentPage = 1; // 重置到第一页
currentPage = 1;
loadLogs();
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{% extends "base_admin.html" %}
{% block page_title %}Provider 管理{% endblock %}
{% block page_css %}<link rel="stylesheet" href="/static/css/providers.css?v=20260319-3">{% endblock %}
{% block page_css %}<link rel="stylesheet" href="/static/css/providers.css?v=20260319-6">{% endblock %}
{% block body_class %}providers-page{% endblock %}
{% block header_title %}Provider 管理{% endblock %}
{% block header_subtitle %}维护 providers 配置,修改后自动重载生效。{% endblock %}
@@ -807,7 +807,7 @@
</div>
</td>
<td class="provider-api-cell" title="${escapeHtml(provider.api)}"><span class="provider-api-text">${escapeHtml(provider.api)}</span></td>
<td>
<td class="provider-model-cell">
<div class="model-tags model-tags-preview">${modelTags}</div>
${modelSummary}
</td>
+49
View File
@@ -155,8 +155,18 @@ class ProviderTemplateTransportTests(unittest.TestCase):
def test_provider_template_contains_transport_field(self) -> None:
template_path = Path(__file__).resolve().parents[1] / "src" / "presentation" / "templates" / "providers.html"
html = template_path.read_text(encoding="utf-8")
css_path = (
Path(__file__).resolve().parents[1]
/ "src"
/ "presentation"
/ "static"
/ "css"
/ "providers.css"
)
css = css_path.read_text(encoding="utf-8")
self.assertIn('id="providerTransport"', html)
self.assertIn('/static/css/providers.css?v=20260319-6', html)
self.assertIn('class="field-help-button"', html)
self.assertIn('data-bs-toggle="tooltip"', html)
self.assertIn('id="fetchModelSelectAllCheckbox"', html)
@@ -168,6 +178,11 @@ class ProviderTemplateTransportTests(unittest.TestCase):
self.assertNotIn('<option value="">-</option>', html)
self.assertIn("WebSocket", html)
self.assertIn("provider-transport-badge", html)
self.assertIn('class="provider-model-cell"', html)
self.assertIn(".providers-page .providers-table td.provider-name-cell {", css)
self.assertIn(".providers-page .providers-table td.provider-api-cell {", css)
self.assertIn(".providers-page .providers-table td.provider-model-cell {", css)
self.assertIn("vertical-align: middle;", css)
self.assertIn("showActionError('保存 Provider'", html)
self.assertIn("showActionError('删除 Provider'", html)
self.assertIn("showActionError('拉取模型'", html)
@@ -199,6 +214,7 @@ class FrontendMessageLocalizationTests(unittest.TestCase):
self.assertIn('/static/js/ui-message.js?v=20260319-1', login_html)
self.assertIn('/static/js/ui-message.js?v=20260319-1', users_html)
self.assertIn('/static/js/ui-message.js?v=20260319-1', index_html)
self.assertIn('/static/css/admin-base.css?v=20260319-3', base_page_html)
self.assertIn('/static/js/theme.js?v=20260319-1', base_page_html)
self.assertIn('aria-label="切换主题"', base_admin_html)
self.assertIn('title="切换主题"', base_admin_html)
@@ -243,5 +259,38 @@ process.stdout.write(output);
self.assertIn("原始信息:https://example.com/v1/models returned 401", stdout)
class DashboardTemplateTests(unittest.TestCase):
def test_index_template_uses_lazy_loaded_tabs(self) -> None:
root = Path(__file__).resolve().parents[1] / "src" / "presentation"
index_html = (
root
/ "templates"
/ "index.html"
).read_text(encoding="utf-8")
index_css = (root / "static" / "css" / "index.css").read_text(encoding="utf-8")
admin_base_css = (root / "static" / "css" / "admin-base.css").read_text(encoding="utf-8")
self.assertIn('/static/css/index.css?v=20260319-4', index_html)
self.assertIn('dashboard-tabs-section', index_html)
self.assertIn('id="dashboardTabBtn_stats"', index_html)
self.assertIn('id="dashboardTabBtn_logs"', index_html)
self.assertIn("function switchDashboardTab(", index_html)
self.assertIn("function loadActiveDashboardTabData()", index_html)
self.assertIn("switchDashboardTab(activeDashboardTab);", index_html)
self.assertIn("if (!['stats', 'logs'].includes(tabName)) {", index_html)
self.assertIn("fetch(`/api/statistics?${params}`, { cache: 'no-store' })", index_html)
self.assertIn("fetch(`/api/request-logs?${params}`, { cache: 'no-store' })", index_html)
self.assertNotIn("dashboardTabDirtyState", index_html)
self.assertNotIn("markDashboardTabsDirty();", index_html)
self.assertIn("document.querySelector('#dashboardTabPanel_logs table')", index_html)
self.assertIn("--dashboard-control-height: 40px;", index_css)
self.assertIn(".dashboard-page .custom-select-trigger {", index_css)
self.assertIn("height: var(--dashboard-control-height);", index_css)
self.assertIn("--nav-tab-hover-bg:", admin_base_css)
self.assertIn(".app-page .header-nav-link:hover {", admin_base_css)
self.assertIn("background: var(--nav-tab-hover-bg);", admin_base_css)
self.assertIn("border-color: var(--nav-tab-hover-border);", index_css)
if __name__ == "__main__":
unittest.main()