move chat whitelist toggle to users page

This commit is contained in:
Von committed 2026-03-20 13:54:51 +08:00
1 parent 247ec9ff78
commit a4d948ff62
5 files changed
+153 -147

No files matched your search

-86
View File
@@ -28,68 +28,6 @@
gap: 10px;
}
.providers-page .toolbar-setting-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: #fff;
box-shadow: var(--shadow-card);
}
.providers-page .toolbar-setting-card + .toolbar-card {
margin-top: 16px;
}
.providers-page .toolbar-setting-copy {
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.providers-page .toolbar-setting-label {
color: var(--text-primary);
font-size: 13px;
font-weight: 700;
}
.providers-page .toolbar-setting-hint {
color: var(--text-secondary);
font-size: 12px;
line-height: 1.45;
}
.providers-page .toolbar-setting-action {
display: inline-flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.providers-page .toolbar-setting-status {
color: var(--text-secondary);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.providers-page .toolbar-setting-card[data-enabled="true"] .toolbar-setting-status {
color: var(--success);
}
.providers-page .toolbar-setting-card[data-enabled="true"] {
border-color: rgba(28, 164, 113, 0.22);
background: rgba(28, 164, 113, 0.06);
}
.providers-page .toolbar-setting-card[data-saving="true"] .toolbar-setting-status {
color: var(--primary-active);
}
.providers-page .toolbar-pill {
display: inline-flex;
align-items: center;
@@ -201,25 +139,11 @@
color: currentColor;
}
:root[data-theme="dark"] .providers-page .toolbar-setting-card {
border-color: #313b47;
background: #161b22;
}
:root[data-theme="dark"] .providers-page .toolbar-card {
border-color: #313b47;
background: #161b22;
}
:root[data-theme="dark"] .providers-page .toolbar-setting-card[data-enabled="true"] {
border-color: rgba(46, 160, 104, 0.42);
background: rgba(46, 160, 104, 0.1);
}
:root[data-theme="dark"] .providers-page .toolbar-setting-card[data-saving="true"] .toolbar-setting-status {
color: #9bc7ff;
}
.providers-page .providers-table tbody tr:nth-child(even) td {
background: #fbfdff;
}
@@ -1191,16 +1115,6 @@
white-space: normal;
}
.providers-page .toolbar-setting-card {
align-items: flex-start;
flex-direction: column;
}
.providers-page .toolbar-setting-action {
width: 100%;
justify-content: space-between;
}
.providers-page .provider-form-grid {
grid-template-columns: 1fr;
}
+83
View File
@@ -5,11 +5,76 @@
gap: 14px;
}
.users-page .users-whitelist-section,
.users-page .add-user-section,
.users-page .users-section {
padding: 22px var(--page-card-inset);
}
.users-page .users-whitelist-section {
padding-bottom: 0;
border-bottom: none;
background: transparent;
}
.users-page .whitelist-setting-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: #fff;
box-shadow: var(--shadow-card);
}
.users-page .whitelist-setting-copy {
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.users-page .whitelist-setting-label {
color: var(--text-primary);
font-size: 13px;
font-weight: 700;
}
.users-page .whitelist-setting-hint {
color: var(--text-secondary);
font-size: 12px;
line-height: 1.45;
}
.users-page .whitelist-setting-action {
display: inline-flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.users-page .whitelist-setting-status {
color: var(--text-secondary);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.users-page .whitelist-setting-card[data-enabled="true"] .whitelist-setting-status {
color: var(--success);
}
.users-page .whitelist-setting-card[data-enabled="true"] {
border-color: rgba(28, 164, 113, 0.22);
background: rgba(28, 164, 113, 0.06);
}
.users-page .whitelist-setting-card[data-saving="true"] .whitelist-setting-status {
color: var(--primary-active);
}
.users-page .add-user-section {
padding: 22px var(--page-card-inset) 0;
border-bottom: none;
@@ -166,6 +231,20 @@
background: #161b22;
}
:root[data-theme="dark"] .users-page .whitelist-setting-card {
border-color: #313b47;
background: #161b22;
}
:root[data-theme="dark"] .users-page .whitelist-setting-card[data-enabled="true"] {
border-color: rgba(46, 160, 104, 0.42);
background: rgba(46, 160, 104, 0.1);
}
:root[data-theme="dark"] .users-page .whitelist-setting-card[data-saving="true"] .whitelist-setting-status {
color: #9bc7ff;
}
.users-page .form-group {
flex: 1;
margin-bottom: 0;
@@ -706,6 +785,7 @@
@media (max-width: 900px) {
.users-page .header,
.users-page .users-whitelist-section,
.users-page .add-user-section,
.users-page .users-section {
padding-left: 16px;
@@ -718,6 +798,7 @@
}
.users-page .section-toolbar,
.users-page .whitelist-setting-card,
.users-page .form-row {
flex-wrap: wrap;
}
@@ -733,12 +814,14 @@
}
@media (max-width: 640px) {
.users-page .users-whitelist-section,
.users-page .add-user-section,
.users-page .users-section {
padding-left: 12px;
padding-right: 12px;
}
.users-page .whitelist-setting-card,
.users-page .form-row {
flex-direction: column;
align-items: stretch;
-60
View File
@@ -8,19 +8,6 @@
{% block content %}
<section class="providers-toolbar-section">
<div class="toolbar-setting-card" id="chatWhitelistCard">
<div class="toolbar-setting-copy">
<span class="toolbar-setting-label">Chat 白名单</span>
<span class="toolbar-setting-hint">控制 Provider Chat 接口是否启用白名单限制,启用后仅允许白名单访问的用户可访问Chat</span>
</div>
<div class="toolbar-setting-action">
<span class="toolbar-setting-status" id="chatWhitelistStatus">已禁用</span>
<label class="switch toolbar-switch" aria-label="切换 Chat 白名单">
<input type="checkbox" id="chatWhitelistToggle" onchange="toggleChatWhitelist(this.checked)">
<span class="slider"></span>
</label>
</div>
</div>
<div class="toolbar-card">
<div class="toolbar-summary">
<span class="toolbar-pill toolbar-pill-primary">共 <strong id="providerCount">0</strong> 个 Provider</span>
@@ -192,7 +179,6 @@
{% block page_scripts %}
<script src="/static/js/ui-message.js?v=20260319-1"></script>
<script>
const initialChatWhitelistEnabled = {{ 'true' if chat_whitelist_enabled else 'false' }};
let providers = [];
let editingProviderName = null;
let providerModalInstance = null;
@@ -201,8 +187,6 @@
let activeModelProvider = null;
let pendingDeleteProviderName = null;
let deletingProviderName = null;
let chatWhitelistEnabled = initialChatWhitelistEnabled;
let chatWhitelistSaving = false;
let fetchedModelCandidates = [];
let fetchedModelSelection = new Set();
let fetchedModelExistingSelection = new Set();
@@ -308,49 +292,6 @@
popover.style.setProperty('--popover-arrow-left', `${arrowLeft}px`);
}
function updateChatWhitelistUi() {
const card = document.getElementById('chatWhitelistCard');
const status = document.getElementById('chatWhitelistStatus');
const toggle = document.getElementById('chatWhitelistToggle');
if (!card || !status || !toggle) return;
card.dataset.enabled = chatWhitelistEnabled ? 'true' : 'false';
card.dataset.saving = chatWhitelistSaving ? 'true' : 'false';
toggle.checked = !!chatWhitelistEnabled;
toggle.disabled = !!chatWhitelistSaving;
status.textContent = chatWhitelistSaving
? '保存中...'
: (chatWhitelistEnabled ? '已启用' : '已禁用');
}
async function toggleChatWhitelist(enabled) {
const previous = chatWhitelistEnabled;
chatWhitelistEnabled = !!enabled;
chatWhitelistSaving = true;
updateChatWhitelistUi();
try {
const response = await fetch('/api/providers/chat-whitelist', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: !!enabled })
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || '更新 Chat 白名单失败');
}
chatWhitelistEnabled = !!result.enabled;
showMessage(`Chat 白名单已${chatWhitelistEnabled ? '启用' : '禁用'}`, 'success');
} catch (error) {
console.error('更新 Chat 白名单失败:', error);
chatWhitelistEnabled = previous;
showActionError('更新 Chat 白名单', error, { fallback: '更新 Chat 白名单失败' });
} finally {
chatWhitelistSaving = false;
updateChatWhitelistUi();
}
}
function getModelPreview(models, limit = 6) {
const items = Array.isArray(models) ? models : [];
const visible = items.slice(0, limit);
@@ -964,7 +905,6 @@
document.addEventListener('DOMContentLoaded', function() {
initThemeToggle();
initTooltips();
updateChatWhitelistUi();
const modalElement = document.getElementById('providerModal');
providerModalInstance = new bootstrap.Modal(modalElement);
modalElement.addEventListener('hidden.bs.modal', function() {
+65 -1
View File
@@ -7,6 +7,22 @@
{% block header_subtitle %}统一管理白名单用户与 IP{% endblock %}
{% block content %}
<section class="users-whitelist-section">
<div class="whitelist-setting-card" id="chatWhitelistCard">
<div class="whitelist-setting-copy">
<span class="whitelist-setting-label">Chat 白名单</span>
<span class="whitelist-setting-hint">控制 Provider Chat 接口是否启用白名单限制,启用后仅允许白名单访问的用户可访问 Chat</span>
</div>
<div class="whitelist-setting-action">
<span class="whitelist-setting-status" id="chatWhitelistStatus">已禁用</span>
<label class="switch" aria-label="切换 Chat 白名单">
<input type="checkbox" id="chatWhitelistToggle" onchange="toggleChatWhitelist(this.checked)">
<span class="slider"></span>
</label>
</div>
</div>
</section>
<section class="add-user-section">
<div class="form-row">
<div class="form-group">
@@ -119,7 +135,9 @@
{% block page_scripts %}
<script src="/static/js/ui-message.js?v=20260319-1"></script>
<script>
const whitelistControlEnabled = {{ 'true' if chat_whitelist_enabled else 'false' }};
const initialWhitelistControlEnabled = {{ 'true' if chat_whitelist_enabled else 'false' }};
let whitelistControlEnabled = initialWhitelistControlEnabled;
let chatWhitelistSaving = false;
let users = [];
let editingUserId = null;
let currentPage = 1;
@@ -167,6 +185,51 @@
return `<span class="numeric-value" title="${full}">${display}</span>`;
}
function updateChatWhitelistUi() {
const card = document.getElementById('chatWhitelistCard');
const status = document.getElementById('chatWhitelistStatus');
const toggle = document.getElementById('chatWhitelistToggle');
if (!card || !status || !toggle) return;
card.dataset.enabled = whitelistControlEnabled ? 'true' : 'false';
card.dataset.saving = chatWhitelistSaving ? 'true' : 'false';
toggle.checked = !!whitelistControlEnabled;
toggle.disabled = !!chatWhitelistSaving;
status.textContent = chatWhitelistSaving
? '保存中...'
: (whitelistControlEnabled ? '已启用' : '已禁用');
}
async function toggleChatWhitelist(enabled) {
const previous = whitelistControlEnabled;
whitelistControlEnabled = !!enabled;
chatWhitelistSaving = true;
updateChatWhitelistUi();
renderUsers();
try {
const response = await fetch('/api/providers/chat-whitelist', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: !!enabled })
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || '更新 Chat 白名单失败');
}
whitelistControlEnabled = !!result.enabled;
showMessage(`Chat 白名单已${whitelistControlEnabled ? '启用' : '禁用'}`, 'success');
} catch (error) {
console.error('更新 Chat 白名单失败:', error);
whitelistControlEnabled = previous;
showActionError('更新 Chat 白名单', error, { fallback: '更新 Chat 白名单失败' });
} finally {
chatWhitelistSaving = false;
updateChatWhitelistUi();
renderUsers();
}
}
function syncCustomSelectValue(selectId) {
const nativeSelect = document.getElementById(selectId);
const customSelect = document.querySelector(`.custom-select[data-for="${selectId}"]`);
@@ -261,6 +324,7 @@
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
initThemeToggle();
updateChatWhitelistUi();
setupCustomSelect('pageSizeSelect');
initUserSearch();
loadUsers();
+5
View File
@@ -169,6 +169,8 @@ 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")
users_template_path = Path(__file__).resolve().parents[1] / "src" / "presentation" / "templates" / "users.html"
users_html = users_template_path.read_text(encoding="utf-8")
css_path = (
Path(__file__).resolve().parents[1]
/ "src"
@@ -202,6 +204,9 @@ class ProviderTemplateTransportTests(unittest.TestCase):
self.assertIn("showActionError('拉取模型'", html)
self.assertNotIn('id="chatWhitelistToggle"', html)
self.assertIn('id="chatWhitelistToggle"', users_html)
def test_provider_model_list_tidy_sorts_and_manual_cleanup_is_explicit(self) -> None:
template_path = Path(__file__).resolve().parents[1] / "src" / "presentation" / "templates" / "providers.html"
html = template_path.read_text(encoding="utf-8")