This commit is contained in:
XOF
2025-11-20 12:24:05 +08:00
commit f28bdc751f
164 changed files with 64248 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
// frontend/js/pages/keys/addApiModal.js
// [REFACTORED] 引入全局的 taskCenterManager 和 modalManager
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 AddApiModal {
constructor({ onImportSuccess }) {
this.modalId = 'add-api-modal';
this.onImportSuccess = onImportSuccess;
this.activeGroupId = null;
this.elements = {
modal: document.getElementById(this.modalId),
title: document.getElementById('add-api-modal-title'),
inputView: document.getElementById('add-api-input-view'),
textarea: document.getElementById('api-add-textarea'),
importBtn: document.getElementById('add-api-import-btn'),
validateCheckbox: document.getElementById('validate-on-import-checkbox'),
};
if (!this.elements.modal) {
throw new Error(`Modal with id "${this.modalId}" not found.`);
}
this._initEventListeners();
}
open(activeGroupId) {
if (!activeGroupId) {
console.error("Cannot open AddApiModal: activeGroupId is required.");
return;
}
this.activeGroupId = activeGroupId;
this._reset();
modalManager.show(this.modalId);
}
_initEventListeners() {
this.elements.importBtn?.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.importBtn.disabled = true;
this.elements.importBtn.innerHTML = `<i class="fas fa-spinner fa-spin mr-2"></i>正在启动...`;
const addKeysTask = {
start: async () => {
const shouldValidate = this.elements.validateCheckbox.checked;
const response = await apiKeyManager.addKeysToGroup(this.activeGroupId, cleanedKeys.join('\n'), shouldValidate);
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 newlyLinked = result.newly_linked_count || 0;
const alreadyLinked = result.already_linked_count || 0;
const summaryTitle = `批量链接 ${newlyLinked} Key已跳过 ${alreadyLinked}`;
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">${data.total}</span></p>
<p class="flex justify-between"><span>分组中已存在 (跳过):</span> <span class="font-semibold">${alreadyLinked}</span></p>
<p class="flex justify-between font-bold"><span>新增链接:</span> <span>${newlyLinked}</span></p>
</div>
</div>
</div>
</div>
`;
} else if (!data.is_running && data.error) { // --- ERROR state ---
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 ---
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>`;
},
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.onImportSuccess) this.onImportSuccess(); // (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');
}
};
// Pass the entire definition to the dispatcher
taskCenterManager.startTask(addKeysTask);
modalManager.hide(this.modalId);
this._reset();
}
_reset() {
// [REMOVED] 不再需要管理 resultView
this.elements.title.textContent = '批量添加 API Keys';
this.elements.inputView.classList.remove('hidden');
this.elements.textarea.value = '';
this.elements.textarea.disabled = false;
this.elements.importBtn.disabled = false;
this.elements.importBtn.innerHTML = '导入'; // 使用 innerHTML 避免潜在的 XSS
}
_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)];
}
}