update ui
This commit is contained in:
1 parent
ae8a87e844
commit
806691dba4
7 files changed
+333
-86
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 %}">
|
||||
|
||||
@@ -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()"><</span>
|
||||
<span class="page-info" id="pageInfo">第 1 页 / 共 1 页</span>
|
||||
<span id="nextBtn" class="page-nav-text disabled" onclick="nextPage()">></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()"><</span>
|
||||
<span class="page-info" id="pageInfo">第 1 页 / 共 1 页</span>
|
||||
<span id="nextBtn" class="page-nav-text disabled" onclick="nextPage()">></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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user