Initial commit
This commit is contained in:
165
frontend/js/pages/keys/deleteApiModal.js
Normal file
165
frontend/js/pages/keys/deleteApiModal.js
Normal file
@@ -0,0 +1,165 @@
|
||||
// frontend/js/pages/keys/deleteApiModal.js
|
||||
|
||||
import { modalManager } from '../../components/ui.js';
|
||||
import { taskCenterManager, toastManager } from '../../components/taskCenter.js';
|
||||
import { apiKeyManager } from '../../components/apiKeyManager.js';
|
||||
import { isValidApiKeyFormat } from '../../utils/utils.js';
|
||||
|
||||
export default class DeleteApiModal {
|
||||
constructor({ onDeleteSuccess }) {
|
||||
this.modalId = 'delete-api-modal';
|
||||
this.onDeleteSuccess = onDeleteSuccess;
|
||||
this.activeGroupId = null;
|
||||
|
||||
this.elements = {
|
||||
modal: document.getElementById(this.modalId),
|
||||
textarea: document.getElementById('api-delete-textarea'),
|
||||
deleteBtn: document.getElementById(this.modalId).querySelector('.modal-btn-danger'),
|
||||
};
|
||||
|
||||
if (!this.elements.modal) {
|
||||
throw new Error(`Modal with id "${this.modalId}" not found.`);
|
||||
}
|
||||
|
||||
this._initEventListeners();
|
||||
}
|
||||
|
||||
open(activeGroupId) {
|
||||
if (!activeGroupId) {
|
||||
console.error("Cannot open DeleteApiModal: activeGroupId is required.");
|
||||
return;
|
||||
}
|
||||
this.activeGroupId = activeGroupId;
|
||||
this._reset();
|
||||
modalManager.show(this.modalId);
|
||||
}
|
||||
|
||||
_initEventListeners() {
|
||||
this.elements.deleteBtn?.addEventListener('click', this._handleSubmit.bind(this));
|
||||
|
||||
const closeAction = () => {
|
||||
this._reset();
|
||||
modalManager.hide(this.modalId);
|
||||
};
|
||||
|
||||
const closeTriggers = this.elements.modal.querySelectorAll(`[data-modal-close="${this.modalId}"]`);
|
||||
closeTriggers.forEach(trigger => trigger.addEventListener('click', closeAction));
|
||||
this.elements.modal.addEventListener('click', (event) => {
|
||||
if (event.target === this.elements.modal) closeAction();
|
||||
});
|
||||
}
|
||||
|
||||
async _handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
const cleanedKeys = this._parseAndCleanKeys(this.elements.textarea.value);
|
||||
if (cleanedKeys.length === 0) {
|
||||
alert('没有检测到有效的API Keys。');
|
||||
return;
|
||||
}
|
||||
this.elements.deleteBtn.disabled = true;
|
||||
this.elements.deleteBtn.innerHTML = `<i class="fas fa-spinner fa-spin mr-2"></i>正在启动...`;
|
||||
const deleteKeysTask = {
|
||||
start: async () => {
|
||||
const response = await apiKeyManager.unlinkKeysFromGroup(this.activeGroupId, cleanedKeys.join('\n'));
|
||||
if (!response.success || !response.data) throw new Error(response.message || '启动解绑任务失败。');
|
||||
return response.data;
|
||||
},
|
||||
poll: async (taskId) => {
|
||||
return await apiKeyManager.getTaskStatus(taskId, { noCache: true });
|
||||
},
|
||||
renderTaskCenterItem: (data, timestamp, formatTimeAgo) => {
|
||||
const timeAgo = formatTimeAgo(timestamp);
|
||||
let contentHtml = '';
|
||||
if (!data.is_running && !data.error) { // --- SUCCESS state ---
|
||||
const result = data.result || {};
|
||||
const unlinked = result.unlinked_count || 0;
|
||||
const deleted = result.hard_deleted_count || 0;
|
||||
const notFound = result.not_found_count || 0;
|
||||
const totalInput = data.total;
|
||||
const summaryTitle = `解绑 ${unlinked} Key,清理 ${deleted}`;
|
||||
|
||||
// [MODIFIED] Applied Flexbox layout for proper spacing.
|
||||
contentHtml = `
|
||||
<div class="task-item-main">
|
||||
<div class="task-item-icon-summary text-green-500"><i class="fas fa-check-circle"></i></div>
|
||||
<div class="task-item-content flex-grow">
|
||||
<div class="flex justify-between items-center cursor-pointer" data-task-toggle>
|
||||
<p class="task-item-title">${summaryTitle}</p>
|
||||
<i class="fas fa-chevron-down task-toggle-icon"></i>
|
||||
</div>
|
||||
<div class="task-details-content collapsed" data-task-content>
|
||||
<div class="task-details-body space-y-1">
|
||||
<p class="flex justify-between"><span>有效输入:</span> <span class="font-semibold">${totalInput}</span></p>
|
||||
<p class="flex justify-between"><span>未在分组中找到:</span> <span class="font-semibold">${notFound}</span></p>
|
||||
<p class="flex justify-between"><span>从分组中解绑:</span> <span class="font-semibold">${unlinked}</span></p>
|
||||
<p class="flex justify-between font-bold"><span>彻底清理孤立Key:</span> <span>${deleted}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (!data.is_running && data.error) { // --- ERROR state ---
|
||||
// [MODIFIED] Applied Flexbox layout for proper spacing.
|
||||
contentHtml = `
|
||||
<div class="task-item-main">
|
||||
<div class="task-item-icon-summary text-red-500"><i class="fas fa-times-circle"></i></div>
|
||||
<div class="task-item-content flex-grow">
|
||||
<p class="task-item-title">批量删除失败</p>
|
||||
<p class="task-item-status text-red-500 truncate" title="${data.error || '未知错误'}">
|
||||
${data.error || '未知错误'}
|
||||
</p>
|
||||
</div>
|
||||
</div>`;
|
||||
} else { // --- RUNNING state ---
|
||||
// [MODIFIED] Applied Flexbox layout with gap for spacing.
|
||||
// [FIX] Replaced 'fa-spin' with Tailwind's 'animate-spin' for reliable animation.
|
||||
contentHtml = `
|
||||
<div class="task-item-main gap-3">
|
||||
<div class="task-item-icon task-item-icon-running"><i class="fas fa-spinner animate-spin"></i></div>
|
||||
<div class="task-item-content flex-grow">
|
||||
<p class="task-item-title">批量删除 ${data.total} 个API Key</p>
|
||||
<p class="task-item-status">运行中...</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
return `${contentHtml}<div class="task-item-timestamp">${timeAgo}</div>`;
|
||||
},
|
||||
// he Toast is now solely responsible for showing real-time progress.
|
||||
renderToastNarrative: (data, oldData, toastManager) => {
|
||||
const toastId = `task-${data.id}`;
|
||||
const progress = data.total > 0 ? (data.processed / data.total) * 100 : 0;
|
||||
// It just reports the current progress, that's its only job.
|
||||
toastManager.showProgressToast(toastId, `批量删除Key`, '处理中', progress); // (Change title for delete modal)
|
||||
},
|
||||
|
||||
// This now ONLY shows the FINAL summary toast, after everything else is done.
|
||||
onSuccess: (data) => {
|
||||
if (this.onDeleteSuccess) this.onDeleteSuccess(); // (Or onDeleteSuccess)
|
||||
const newlyLinked = data.result?.newly_linked_count || 0; // (Or unlinked_count)
|
||||
toastManager.show(`任务完成!成功删除 ${newlyLinked} 个Key。`, 'success'); // (Adjust text for delete)
|
||||
},
|
||||
|
||||
// This is the final error handler.
|
||||
onError: (data) => {
|
||||
toastManager.show(`任务失败: ${data.error || '未知错误'}`, 'error');
|
||||
}
|
||||
};
|
||||
taskCenterManager.startTask(deleteKeysTask);
|
||||
|
||||
modalManager.hide(this.modalId);
|
||||
this._reset();
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this.elements.textarea.value = '';
|
||||
this.elements.deleteBtn.disabled = false;
|
||||
this.elements.deleteBtn.innerHTML = '删除';
|
||||
}
|
||||
|
||||
_parseAndCleanKeys(text) {
|
||||
const keys = text.replace(/[,;]/g, ' ').split(/[\s\n]+/);
|
||||
const cleanedKeys = keys.map(key => key.trim()).filter(key => isValidApiKeyFormat(key));
|
||||
return [...new Set(cleanedKeys)];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user