优化删除用户、provider时的提醒
This commit is contained in:
1 parent
4131a8e7f2
commit
b635bf5128
4 files changed
+607
-7
No files matched your search
@@ -308,6 +308,7 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.providers-page .switch {
|
||||
@@ -449,6 +450,95 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.providers-page .btn-delete.is-active {
|
||||
background: var(--danger-hover);
|
||||
border-color: var(--danger-hover);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-popover {
|
||||
position: fixed;
|
||||
z-index: 1080;
|
||||
width: max-content;
|
||||
max-width: min(188px, calc(100vw - 24px));
|
||||
padding: 9px 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.providers-page .floating-delete-popover[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-popover::after,
|
||||
.providers-page .inline-confirm-popover::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: var(--popover-arrow-left, 50%);
|
||||
transform: translateX(-50%);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-popover[data-placement="top"]::after {
|
||||
top: 100%;
|
||||
border-width: 6px 6px 0 6px;
|
||||
border-color: var(--line) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-popover[data-placement="top"]::before {
|
||||
top: 100%;
|
||||
margin-top: -1px;
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-popover[data-placement="bottom"]::after {
|
||||
bottom: 100%;
|
||||
border-width: 0 6px 6px 6px;
|
||||
border-color: transparent transparent var(--line) transparent;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-popover[data-placement="bottom"]::before {
|
||||
bottom: 100%;
|
||||
margin-bottom: -1px;
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-color: transparent transparent #fff transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-text {
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-action {
|
||||
min-width: 54px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.providers-page .inline-confirm-action:disabled {
|
||||
opacity: 0.72;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .btn-action {
|
||||
background: #0d1117;
|
||||
}
|
||||
@@ -845,6 +935,33 @@
|
||||
border-color: #30363d !important;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .inline-confirm-popover {
|
||||
border-color: #30363d;
|
||||
background: #161b22;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .inline-confirm-popover::after {
|
||||
border-top-color: #30363d;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .inline-confirm-popover::before {
|
||||
border-top-color: #161b22;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .inline-confirm-popover[data-placement="bottom"]::after {
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: #30363d;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .inline-confirm-popover[data-placement="bottom"]::before {
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: #161b22;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .inline-confirm-action:disabled {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .modal .btn-secondary {
|
||||
border-color: #3b4654;
|
||||
background: linear-gradient(180deg, rgba(28, 36, 49, 0.98), rgba(20, 27, 38, 0.98));
|
||||
|
||||
@@ -289,6 +289,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.users-page .btn-action {
|
||||
@@ -336,6 +337,95 @@
|
||||
border-color: var(--danger-hover);
|
||||
}
|
||||
|
||||
.users-page .btn-delete.is-active {
|
||||
color: #fff;
|
||||
background: var(--danger-hover);
|
||||
border-color: var(--danger-hover);
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-popover {
|
||||
position: fixed;
|
||||
z-index: 1080;
|
||||
width: max-content;
|
||||
max-width: min(188px, calc(100vw - 24px));
|
||||
padding: 9px 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.users-page .floating-delete-popover[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-popover::after,
|
||||
.users-page .inline-confirm-popover::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: var(--popover-arrow-left, 50%);
|
||||
transform: translateX(-50%);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-popover[data-placement="top"]::after {
|
||||
top: 100%;
|
||||
border-width: 6px 6px 0 6px;
|
||||
border-color: var(--line) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-popover[data-placement="top"]::before {
|
||||
top: 100%;
|
||||
margin-top: -1px;
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-popover[data-placement="bottom"]::after {
|
||||
bottom: 100%;
|
||||
border-width: 0 6px 6px 6px;
|
||||
border-color: transparent transparent var(--line) transparent;
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-popover[data-placement="bottom"]::before {
|
||||
bottom: 100%;
|
||||
margin-bottom: -1px;
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-color: transparent transparent #fff transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-text {
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-action {
|
||||
min-width: 54px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.users-page .inline-confirm-action:disabled {
|
||||
opacity: 0.72;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.users-page .switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -480,6 +570,9 @@
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-soft) !important;
|
||||
background: var(--surface);
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.users-page .modal-header,
|
||||
@@ -488,14 +581,105 @@
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.users-page .modal-header,
|
||||
.users-page .modal-body,
|
||||
.users-page .modal-footer {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.users-page .modal-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.users-page .modal-subtitle {
|
||||
margin-top: 4px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.users-page .modal-footer .btn {
|
||||
min-width: 92px;
|
||||
}
|
||||
|
||||
.users-page .modal .form-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.users-page .modal .btn-secondary {
|
||||
border-color: var(--line-strong);
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.users-page .modal .btn-secondary:hover,
|
||||
.users-page .modal .btn-secondary:focus {
|
||||
border-color: var(--primary);
|
||||
background: #ffffff;
|
||||
color: var(--primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.users-page .btn-close {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.users-page .btn-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.users-page .btn-close:focus {
|
||||
box-shadow: 0 0 0 3px var(--focus);
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .users-page .modal-content,
|
||||
:root[data-theme="dark"] .users-page .modal-header,
|
||||
:root[data-theme="dark"] .users-page .modal-body,
|
||||
:root[data-theme="dark"] .users-page .modal-footer {
|
||||
background: #161b22;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .users-page .modal-content {
|
||||
border-color: #30363d;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .users-page .modal .btn-secondary {
|
||||
border-color: #3b4654;
|
||||
background: linear-gradient(180deg, rgba(28, 36, 49, 0.98), rgba(20, 27, 38, 0.98));
|
||||
color: #bcc8d8;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .users-page .btn-close {
|
||||
filter: invert(1) grayscale(1);
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .users-page .inline-confirm-popover {
|
||||
border-color: #30363d;
|
||||
background: #161b22;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .users-page .inline-confirm-popover::after {
|
||||
border-top-color: #30363d;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .users-page .inline-confirm-popover::before {
|
||||
border-top-color: #161b22;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .users-page .inline-confirm-popover[data-placement="bottom"]::after {
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: #30363d;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .users-page .inline-confirm-popover[data-placement="bottom"]::before {
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: #161b22;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .users-page .inline-confirm-action:disabled {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.users-page .header,
|
||||
.users-page .add-user-section,
|
||||
|
||||
@@ -166,6 +166,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-confirm-popover floating-delete-popover" id="providerDeletePopover" data-placement="bottom" hidden>
|
||||
<div class="inline-confirm-text">确认删除?</div>
|
||||
<div class="inline-confirm-actions">
|
||||
<button type="button" class="btn-action btn-edit inline-confirm-action" onclick="cancelDeleteProviderConfirm(event)">取消</button>
|
||||
<button type="button" class="btn-action btn-delete inline-confirm-action" id="confirmDeleteProviderBtn" onclick="confirmDeleteProvider(event)">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_scripts %}
|
||||
@@ -178,6 +185,8 @@
|
||||
let fetchModelPickerModalInstance = null;
|
||||
let modelDetailModalInstance = null;
|
||||
let activeModelProvider = null;
|
||||
let pendingDeleteProviderName = null;
|
||||
let deletingProviderName = null;
|
||||
let chatWhitelistEnabled = initialChatWhitelistEnabled;
|
||||
let chatWhitelistSaving = false;
|
||||
let fetchedModelCandidates = [];
|
||||
@@ -197,6 +206,94 @@
|
||||
return decodeURIComponent(encodedName);
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(event) {
|
||||
if (!event.target.closest('.inline-confirm-wrap') && !event.target.closest('#providerDeletePopover')) {
|
||||
closeDeleteProviderConfirm();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeDeleteProviderConfirm();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
closeDeleteProviderConfirm(false);
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
closeDeleteProviderConfirm(false);
|
||||
}, true);
|
||||
|
||||
function getProviderDeletePopoverElement() {
|
||||
return document.getElementById('providerDeletePopover');
|
||||
}
|
||||
|
||||
function getProviderDeleteTrigger(name) {
|
||||
const triggerKey = encodeURIComponent(name);
|
||||
return Array.from(document.querySelectorAll('[data-provider-delete-trigger]'))
|
||||
.find(element => element.dataset.providerDeleteTrigger === triggerKey) || null;
|
||||
}
|
||||
|
||||
function hideProviderDeletePopover() {
|
||||
const popover = getProviderDeletePopoverElement();
|
||||
if (!popover) return;
|
||||
popover.hidden = true;
|
||||
popover.style.removeProperty('top');
|
||||
popover.style.removeProperty('left');
|
||||
popover.style.removeProperty('--popover-arrow-left');
|
||||
popover.dataset.placement = 'bottom';
|
||||
}
|
||||
|
||||
function syncDeleteProviderPopover() {
|
||||
const popover = getProviderDeletePopoverElement();
|
||||
if (!popover) return;
|
||||
|
||||
if (pendingDeleteProviderName === null) {
|
||||
hideProviderDeletePopover();
|
||||
return;
|
||||
}
|
||||
|
||||
const trigger = getProviderDeleteTrigger(pendingDeleteProviderName);
|
||||
if (!trigger) {
|
||||
pendingDeleteProviderName = null;
|
||||
deletingProviderName = null;
|
||||
hideProviderDeletePopover();
|
||||
return;
|
||||
}
|
||||
|
||||
popover.hidden = false;
|
||||
const confirmButton = document.getElementById('confirmDeleteProviderBtn');
|
||||
if (confirmButton) {
|
||||
confirmButton.disabled = deletingProviderName === pendingDeleteProviderName;
|
||||
confirmButton.textContent = deletingProviderName === pendingDeleteProviderName ? '删除中...' : '确认';
|
||||
}
|
||||
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const popoverRect = popover.getBoundingClientRect();
|
||||
const viewportPadding = 12;
|
||||
const offset = 10;
|
||||
const canPlaceBottom = triggerRect.bottom + offset + popoverRect.height <= window.innerHeight - viewportPadding;
|
||||
const shouldPlaceBottom = canPlaceBottom || triggerRect.top < popoverRect.height + viewportPadding + offset;
|
||||
const top = shouldPlaceBottom
|
||||
? Math.min(window.innerHeight - popoverRect.height - viewportPadding, triggerRect.bottom + offset)
|
||||
: Math.max(viewportPadding, triggerRect.top - popoverRect.height - offset);
|
||||
const left = Math.min(
|
||||
window.innerWidth - popoverRect.width - viewportPadding,
|
||||
Math.max(viewportPadding, triggerRect.left + ((triggerRect.width - popoverRect.width) / 2))
|
||||
);
|
||||
const arrowLeft = Math.min(
|
||||
popoverRect.width - 18,
|
||||
Math.max(18, triggerRect.left + (triggerRect.width / 2) - left)
|
||||
);
|
||||
|
||||
popover.dataset.placement = shouldPlaceBottom ? 'bottom' : 'top';
|
||||
popover.style.top = `${top}px`;
|
||||
popover.style.left = `${left}px`;
|
||||
popover.style.setProperty('--popover-arrow-left', `${arrowLeft}px`);
|
||||
}
|
||||
|
||||
function updateChatWhitelistUi() {
|
||||
const card = document.getElementById('chatWhitelistCard');
|
||||
const status = document.getElementById('chatWhitelistStatus');
|
||||
@@ -576,6 +673,9 @@
|
||||
|
||||
async function loadProviders() {
|
||||
try {
|
||||
pendingDeleteProviderName = null;
|
||||
deletingProviderName = null;
|
||||
hideProviderDeletePopover();
|
||||
const response = await fetch('/api/providers');
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
@@ -616,6 +716,7 @@
|
||||
providers.forEach(provider => {
|
||||
const models = Array.isArray(provider.model_list) ? provider.model_list : [];
|
||||
const preview = getModelPreview(models);
|
||||
const isDeletePending = pendingDeleteProviderName === provider.name;
|
||||
const modelTags = preview.visible.length
|
||||
? preview.visible.map(model => `<span class="model-tag">${escapeHtml(model)}</span>`).join('')
|
||||
: '<span class="muted-text">未配置</span>';
|
||||
@@ -640,7 +741,14 @@
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-action btn-edit" onclick="openEditModal(decodeProviderName('${encodeURIComponent(provider.name)}'))">编辑</button>
|
||||
<button class="btn-action btn-delete" onclick="deleteProvider(decodeProviderName('${encodeURIComponent(provider.name)}'))">删除</button>
|
||||
<div class="inline-confirm-wrap">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-action btn-delete ${isDeletePending ? 'is-active' : ''}"
|
||||
data-provider-delete-trigger="${encodeURIComponent(provider.name)}"
|
||||
onclick="toggleDeleteProviderConfirm(decodeProviderName('${encodeURIComponent(provider.name)}'), event)"
|
||||
>删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -649,6 +757,7 @@
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
syncDeleteProviderPopover();
|
||||
}
|
||||
|
||||
async function saveProvider() {
|
||||
@@ -687,8 +796,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProvider(name) {
|
||||
if (!confirm(`确定删除 provider "${name}" 吗?`)) return;
|
||||
function toggleDeleteProviderConfirm(name, event) {
|
||||
event?.stopPropagation();
|
||||
if (deletingProviderName !== null) return;
|
||||
|
||||
pendingDeleteProviderName = pendingDeleteProviderName === name ? null : name;
|
||||
if (pendingDeleteProviderName !== name) {
|
||||
deletingProviderName = null;
|
||||
}
|
||||
renderProviders();
|
||||
}
|
||||
|
||||
function cancelDeleteProviderConfirm(event) {
|
||||
event?.stopPropagation();
|
||||
closeDeleteProviderConfirm(true);
|
||||
}
|
||||
|
||||
function closeDeleteProviderConfirm(shouldRender = true) {
|
||||
if (pendingDeleteProviderName === null && deletingProviderName === null) return;
|
||||
if (deletingProviderName !== null) {
|
||||
return;
|
||||
}
|
||||
pendingDeleteProviderName = null;
|
||||
hideProviderDeletePopover();
|
||||
if (shouldRender) {
|
||||
renderProviders();
|
||||
} else {
|
||||
document.querySelectorAll('[data-provider-delete-trigger].is-active').forEach(element => {
|
||||
element.classList.remove('is-active');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDeleteProvider(event) {
|
||||
event?.stopPropagation();
|
||||
const name = pendingDeleteProviderName;
|
||||
if (pendingDeleteProviderName !== name || deletingProviderName === name) return;
|
||||
|
||||
deletingProviderName = name;
|
||||
syncDeleteProviderPopover();
|
||||
renderProviders();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/providers/${encodeURIComponent(name)}`, {
|
||||
@@ -698,10 +845,16 @@
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || '删除 provider 失败');
|
||||
}
|
||||
pendingDeleteProviderName = null;
|
||||
deletingProviderName = null;
|
||||
hideProviderDeletePopover();
|
||||
showMessage('Provider 删除成功,配置已重载', 'success');
|
||||
await loadProviders();
|
||||
} catch (error) {
|
||||
console.error('删除 provider 失败:', error);
|
||||
deletingProviderName = null;
|
||||
syncDeleteProviderPopover();
|
||||
renderProviders();
|
||||
showMessage(error.message || '删除 provider 失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-confirm-popover floating-delete-popover" id="userDeletePopover" data-placement="bottom" hidden>
|
||||
<div class="inline-confirm-text">确认删除?</div>
|
||||
<div class="inline-confirm-actions">
|
||||
<button type="button" class="btn-action btn-edit inline-confirm-action" onclick="cancelDeleteUserConfirm(event)">取消</button>
|
||||
<button type="button" class="btn-action btn-delete inline-confirm-action" id="confirmDeleteUserBtn" onclick="confirmDeleteUser(event)">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_scripts %}
|
||||
@@ -121,6 +128,8 @@
|
||||
let currentSearchKeyword = '';
|
||||
let activeCustomSelect = null;
|
||||
let editModalInstance = null;
|
||||
let pendingDeleteUserId = null;
|
||||
let deletingUserId = null;
|
||||
let usersSortState = {
|
||||
key: 'total_tokens',
|
||||
direction: 'desc',
|
||||
@@ -337,17 +346,98 @@
|
||||
if (!event.target.closest('.custom-select')) {
|
||||
closeAllCustomSelects();
|
||||
}
|
||||
if (!event.target.closest('.inline-confirm-wrap') && !event.target.closest('#userDeletePopover')) {
|
||||
closeDeleteUserConfirm();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeAllCustomSelects();
|
||||
closeDeleteUserConfirm();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
closeDeleteUserConfirm(false);
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
closeDeleteUserConfirm(false);
|
||||
}, true);
|
||||
|
||||
function getUserDeletePopoverElement() {
|
||||
return document.getElementById('userDeletePopover');
|
||||
}
|
||||
|
||||
function getUserDeleteTrigger(userId) {
|
||||
return document.querySelector(`[data-user-delete-trigger="${userId}"]`);
|
||||
}
|
||||
|
||||
function hideUserDeletePopover() {
|
||||
const popover = getUserDeletePopoverElement();
|
||||
if (!popover) return;
|
||||
popover.hidden = true;
|
||||
popover.style.removeProperty('top');
|
||||
popover.style.removeProperty('left');
|
||||
popover.style.removeProperty('--popover-arrow-left');
|
||||
popover.dataset.placement = 'bottom';
|
||||
}
|
||||
|
||||
function syncDeleteUserPopover() {
|
||||
const popover = getUserDeletePopoverElement();
|
||||
if (!popover) return;
|
||||
|
||||
if (pendingDeleteUserId === null) {
|
||||
hideUserDeletePopover();
|
||||
return;
|
||||
}
|
||||
|
||||
const trigger = getUserDeleteTrigger(pendingDeleteUserId);
|
||||
if (!trigger) {
|
||||
pendingDeleteUserId = null;
|
||||
deletingUserId = null;
|
||||
hideUserDeletePopover();
|
||||
return;
|
||||
}
|
||||
|
||||
popover.hidden = false;
|
||||
const confirmButton = document.getElementById('confirmDeleteUserBtn');
|
||||
if (confirmButton) {
|
||||
confirmButton.disabled = deletingUserId === pendingDeleteUserId;
|
||||
confirmButton.textContent = deletingUserId === pendingDeleteUserId ? '删除中...' : '确认';
|
||||
}
|
||||
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const popoverRect = popover.getBoundingClientRect();
|
||||
const viewportPadding = 12;
|
||||
const offset = 10;
|
||||
const canPlaceBottom = triggerRect.bottom + offset + popoverRect.height <= window.innerHeight - viewportPadding;
|
||||
const shouldPlaceBottom = canPlaceBottom || triggerRect.top < popoverRect.height + viewportPadding + offset;
|
||||
const top = shouldPlaceBottom
|
||||
? Math.min(window.innerHeight - popoverRect.height - viewportPadding, triggerRect.bottom + offset)
|
||||
: Math.max(viewportPadding, triggerRect.top - popoverRect.height - offset);
|
||||
const left = Math.min(
|
||||
window.innerWidth - popoverRect.width - viewportPadding,
|
||||
Math.max(viewportPadding, triggerRect.left + ((triggerRect.width - popoverRect.width) / 2))
|
||||
);
|
||||
const arrowLeft = Math.min(
|
||||
popoverRect.width - 18,
|
||||
Math.max(18, triggerRect.left + (triggerRect.width / 2) - left)
|
||||
);
|
||||
|
||||
popover.dataset.placement = shouldPlaceBottom ? 'bottom' : 'top';
|
||||
popover.style.top = `${top}px`;
|
||||
popover.style.left = `${left}px`;
|
||||
popover.style.setProperty('--popover-arrow-left', `${arrowLeft}px`);
|
||||
}
|
||||
|
||||
// 加载用户列表
|
||||
async function loadUsers() {
|
||||
try {
|
||||
pendingDeleteUserId = null;
|
||||
deletingUserId = null;
|
||||
hideUserDeletePopover();
|
||||
const params = new URLSearchParams({
|
||||
page: currentPage,
|
||||
page_size: pageSize
|
||||
@@ -503,6 +593,7 @@
|
||||
|
||||
users.forEach(user => {
|
||||
const whitelistEnabled = !!user.whitelist_access_enabled;
|
||||
const isDeletePending = pendingDeleteUserId === user.id;
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
@@ -523,7 +614,14 @@
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-action btn-edit" onclick="openEditModal(${user.id})">编辑</button>
|
||||
<button class="btn-action btn-delete" onclick="deleteUser(${user.id})">删除</button>
|
||||
<div class="inline-confirm-wrap">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-action btn-delete ${isDeletePending ? 'is-active' : ''}"
|
||||
data-user-delete-trigger="${user.id}"
|
||||
onclick="toggleDeleteUserConfirm(${user.id}, event)"
|
||||
>删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -532,6 +630,7 @@
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
syncDeleteUserPopover();
|
||||
}
|
||||
|
||||
// 更新分页
|
||||
@@ -717,8 +816,46 @@
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
async function deleteUser(userId) {
|
||||
if (!confirm('确定要删除该用户吗?')) return;
|
||||
function toggleDeleteUserConfirm(userId, event) {
|
||||
event?.stopPropagation();
|
||||
if (deletingUserId !== null) return;
|
||||
|
||||
pendingDeleteUserId = pendingDeleteUserId === userId ? null : userId;
|
||||
if (pendingDeleteUserId !== userId) {
|
||||
deletingUserId = null;
|
||||
}
|
||||
renderUsers();
|
||||
}
|
||||
|
||||
function cancelDeleteUserConfirm(event) {
|
||||
event?.stopPropagation();
|
||||
closeDeleteUserConfirm(true);
|
||||
}
|
||||
|
||||
function closeDeleteUserConfirm(shouldRender = true) {
|
||||
if (pendingDeleteUserId === null && deletingUserId === null) return;
|
||||
if (deletingUserId !== null) {
|
||||
return;
|
||||
}
|
||||
pendingDeleteUserId = null;
|
||||
hideUserDeletePopover();
|
||||
if (shouldRender) {
|
||||
renderUsers();
|
||||
} else {
|
||||
document.querySelectorAll('[data-user-delete-trigger].is-active').forEach(element => {
|
||||
element.classList.remove('is-active');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDeleteUser(event) {
|
||||
event?.stopPropagation();
|
||||
const userId = pendingDeleteUserId;
|
||||
if (pendingDeleteUserId !== userId || deletingUserId === userId) return;
|
||||
|
||||
deletingUserId = userId;
|
||||
syncDeleteUserPopover();
|
||||
renderUsers();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`, {
|
||||
@@ -728,13 +865,22 @@
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
pendingDeleteUserId = null;
|
||||
deletingUserId = null;
|
||||
hideUserDeletePopover();
|
||||
showMessage('用户删除成功', 'success');
|
||||
loadUsers();
|
||||
await loadUsers();
|
||||
} else {
|
||||
deletingUserId = null;
|
||||
syncDeleteUserPopover();
|
||||
renderUsers();
|
||||
showMessage(result.error || '删除失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error);
|
||||
deletingUserId = null;
|
||||
syncDeleteUserPopover();
|
||||
renderUsers();
|
||||
showMessage('删除用户失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user