move chat whitelist toggle to users page
This commit is contained in:
1 parent
247ec9ff78
commit
a4d948ff62
5 files changed
+153
-147
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user