AIClient-2-API/static/app/event-handlers.js
hex2077 0816de2ba2 feat(ui): 添加图片放大功能并优化仪表盘布局
实现二维码图片点击放大功能,重构仪表盘顶部布局将联系信息与统计卡片并排显示
添加多语言图片切换功能,根据语言显示不同的赞助和联系方式图片
优化Kiro OAuth流程,增加自动关联凭据到Pools的功能
2025-12-21 21:09:16 +08:00

403 lines
No EOL
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 事件监听器模块
import { elements, autoScroll, setAutoScroll, clearLogs } from './constants.js';
import { showToast } from './utils.js';
import { fileUploadHandler } from './file-upload.js';
import { t } from './i18n.js';
/**
* 初始化所有事件监听器
*/
function initEventListeners() {
// 刷新按钮
if (elements.refreshBtn) {
elements.refreshBtn.addEventListener('click', handleRefresh);
}
// 清空日志
if (elements.clearLogsBtn) {
elements.clearLogsBtn.addEventListener('click', () => {
clearLogs();
if (elements.logsContainer) {
elements.logsContainer.innerHTML = '';
}
showToast(t('common.success'), t('common.refresh.success'), 'success');
});
}
// 自动滚动切换
if (elements.toggleAutoScrollBtn) {
elements.toggleAutoScrollBtn.addEventListener('click', () => {
const newAutoScroll = !autoScroll;
setAutoScroll(newAutoScroll);
elements.toggleAutoScrollBtn.dataset.enabled = newAutoScroll;
const statusText = newAutoScroll ? t('logs.autoScroll.on') : t('logs.autoScroll.off');
elements.toggleAutoScrollBtn.innerHTML = `
<i class="fas fa-arrow-down"></i>
<span data-i18n="${newAutoScroll ? 'logs.autoScroll.on' : 'logs.autoScroll.off'}">${statusText}</span>
`;
});
}
// 保存配置
if (elements.saveConfigBtn) {
elements.saveConfigBtn.addEventListener('click', saveConfiguration);
}
// 重置配置
if (elements.resetConfigBtn) {
elements.resetConfigBtn.addEventListener('click', loadInitialData);
}
// 模型提供商切换
if (elements.modelProvider) {
elements.modelProvider.addEventListener('change', handleProviderChange);
}
// Gemini凭据类型切换
document.querySelectorAll('input[name="geminiCredsType"]').forEach(radio => {
radio.addEventListener('change', handleGeminiCredsTypeChange);
});
// Kiro凭据类型切换
document.querySelectorAll('input[name="kiroCredsType"]').forEach(radio => {
radio.addEventListener('change', handleKiroCredsTypeChange);
});
// 密码显示/隐藏切换
document.querySelectorAll('.password-toggle').forEach(button => {
button.addEventListener('click', handlePasswordToggle);
});
// 生成凭据按钮监听
document.querySelectorAll('.generate-creds-btn').forEach(button => {
button.addEventListener('click', handleGenerateCreds);
});
// 提供商池配置监听
// const providerPoolsInput = document.getElementById('providerPoolsFilePath');
// if (providerPoolsInput) {
// providerPoolsInput.addEventListener('input', handleProviderPoolsConfigChange);
// }
// 日志容器滚动
if (elements.logsContainer) {
elements.logsContainer.addEventListener('scroll', () => {
if (autoScroll) {
const isAtBottom = elements.logsContainer.scrollTop + elements.logsContainer.clientHeight
>= elements.logsContainer.scrollHeight - 5;
if (!isAtBottom) {
setAutoScroll(false);
elements.toggleAutoScrollBtn.dataset.enabled = false;
elements.toggleAutoScrollBtn.innerHTML = `
<i class="fas fa-arrow-down"></i>
<span data-i18n="logs.autoScroll.off">${t('logs.autoScroll.off')}</span>
`;
}
}
});
}
}
/**
* 提供商配置切换处理
*/
function handleProviderChange() {
const selectedProvider = elements.modelProvider?.value;
if (!selectedProvider) return;
const allProviderConfigs = document.querySelectorAll('.provider-config');
// 隐藏所有提供商配置
allProviderConfigs.forEach(config => {
config.style.display = 'none';
});
// 显示当前选中的提供商配置
const targetConfig = document.querySelector(`[data-provider="${selectedProvider}"]`);
if (targetConfig) {
targetConfig.style.display = 'block';
}
}
/**
* Gemini凭据类型切换
* @param {Event} event - 事件对象
*/
function handleGeminiCredsTypeChange(event) {
const selectedType = event.target.value;
const base64Group = document.getElementById('geminiCredsBase64Group');
const fileGroup = document.getElementById('geminiCredsFileGroup');
if (selectedType === 'base64') {
if (base64Group) base64Group.style.display = 'block';
if (fileGroup) fileGroup.style.display = 'none';
} else {
if (base64Group) base64Group.style.display = 'none';
if (fileGroup) fileGroup.style.display = 'block';
}
}
/**
* Kiro凭据类型切换
* @param {Event} event - 事件对象
*/
function handleKiroCredsTypeChange(event) {
const selectedType = event.target.value;
const base64Group = document.getElementById('kiroCredsBase64Group');
const fileGroup = document.getElementById('kiroCredsFileGroup');
if (selectedType === 'base64') {
if (base64Group) base64Group.style.display = 'block';
if (fileGroup) fileGroup.style.display = 'none';
} else {
if (base64Group) base64Group.style.display = 'none';
if (fileGroup) fileGroup.style.display = 'block';
}
}
/**
* 密码显示/隐藏切换处理
* @param {Event} event - 事件对象
*/
function handlePasswordToggle(event) {
const button = event.target.closest('.password-toggle');
if (!button) return;
const targetId = button.getAttribute('data-target');
const input = document.getElementById(targetId);
const icon = button.querySelector('i');
if (!input || !icon) return;
if (input.type === 'password') {
input.type = 'text';
icon.className = 'fas fa-eye-slash';
} else {
input.type = 'password';
icon.className = 'fas fa-eye';
}
}
/**
* 处理生成凭据逻辑
* @param {Event} event - 事件对象
*/
async function handleGenerateCreds(event) {
const button = event.target.closest('.generate-creds-btn');
if (!button) return;
const providerType = button.getAttribute('data-provider');
const targetInputId = button.getAttribute('data-target');
try {
// 如果是 Kiro OAuth先显示认证方式选择对话框
if (providerType === 'claude-kiro-oauth') {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.style.display = 'flex';
modal.innerHTML = `
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3><i class="fas fa-key"></i> <span data-i18n="oauth.kiro.selectMethod">${t('oauth.kiro.selectMethod')}</span></h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="auth-method-options" style="display: flex; flex-direction: column; gap: 12px;">
<!--<button class="auth-method-btn" data-method="google" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-google" style="font-size: 24px; color: #4285f4;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.google">${t('oauth.kiro.google')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.googleDesc">${t('oauth.kiro.googleDesc')}</div>
</div>
</button>
<button class="auth-method-btn" data-method="github" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-github" style="font-size: 24px; color: #333;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.github">${t('oauth.kiro.github')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.githubDesc">${t('oauth.kiro.githubDesc')}</div>
</div>
</button> -->
<button class="auth-method-btn" data-method="builder-id" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-aws" style="font-size: 24px; color: #ff9900;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.awsBuilder">${t('oauth.kiro.awsBuilder')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.awsBuilderDesc">${t('oauth.kiro.awsBuilderDesc')}</div>
</div>
</button>
</div>
</div>
<div class="modal-footer">
<button class="modal-cancel" data-i18n="modal.provider.cancel">${t('modal.provider.cancel')}</button>
</div>
</div>
`;
document.body.appendChild(modal);
const closeModal = () => modal.remove();
modal.querySelector('.modal-close').onclick = closeModal;
modal.querySelector('.modal-cancel').onclick = closeModal;
modal.querySelectorAll('.auth-method-btn').forEach(btn => {
btn.onclick = async () => {
const method = btn.dataset.method;
closeModal();
await proceedWithAuth(providerType, targetInputId, { method });
};
});
return;
}
await proceedWithAuth(providerType, targetInputId, {});
} catch (error) {
console.error('生成凭据失败:', error);
showToast(t('common.error'), t('modal.provider.auth.failed') + `: ${error.message}`, 'error');
}
}
/**
* 实际执行授权逻辑
*/
async function proceedWithAuth(providerType, targetInputId, extraOptions = {}) {
try {
showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info');
// 使用 fileUploadHandler 中的 getProviderKey 获取目录名称
const providerDir = fileUploadHandler.getProviderKey(providerType);
const response = await window.apiClient.post(
`/providers/${encodeURIComponent(providerType)}/generate-auth-url`,
{
saveToConfigs: true,
providerDir: providerDir,
...extraOptions
}
);
if (response.success && response.authUrl) {
// 使用自定义事件监听授权成功,以便自动填充路径
const handleSuccess = (e) => {
const data = e.detail;
if (data.provider === providerType && data.relativePath) {
const input = document.getElementById(targetInputId);
if (input) {
input.value = data.relativePath;
input.dispatchEvent(new Event('input', { bubbles: true }));
showToast(t('common.success'), t('modal.provider.auth.success'), 'success');
}
window.removeEventListener('oauth_success_event', handleSuccess);
}
};
window.addEventListener('oauth_success_event', handleSuccess);
// 调用 provider-manager.js 中的 showAuthModal (假设已在全局作用域或通过某种方式可用)
// 如果不可用,我们需要在 app.js 中导出它
if (window.showAuthModal) {
window.showAuthModal(response.authUrl, response.authInfo);
} else {
// 降级处理:如果在 app.js 中没导出,尝试直接打开
window.open(response.authUrl, '_blank');
showToast(t('common.info'), t('modal.provider.auth.window'), 'info');
}
} else {
showToast(t('common.error'), t('modal.provider.auth.failed'), 'error');
}
} catch (error) {
console.error('生成凭据失败:', error);
showToast(t('common.error'), t('modal.provider.auth.failed') + `: ${error.message}`, 'error');
}
}
/**
* 提供商池配置变化处理
* @param {Event} event - 事件对象
*/
function handleProviderPoolsConfigChange(event) {
const filePath = event.target.value.trim();
const providersMenuItem = document.querySelector('.nav-item[data-section="providers"]');
if (filePath) {
// 显示提供商池菜单
if (providersMenuItem) providersMenuItem.style.display = 'flex';
} else {
// 隐藏提供商池菜单
if (providersMenuItem) providersMenuItem.style.display = 'none';
// 如果当前在提供商池页面,切换到仪表盘
if (providersMenuItem && providersMenuItem.classList.contains('active')) {
const dashboardItem = document.querySelector('.nav-item[data-section="dashboard"]');
const dashboardSection = document.getElementById('dashboard');
// 更新导航状态
document.querySelectorAll('.nav-item').forEach(nav => nav.classList.remove('active'));
document.querySelectorAll('.section').forEach(section => section.classList.remove('active'));
if (dashboardItem) dashboardItem.classList.add('active');
if (dashboardSection) dashboardSection.classList.add('active');
}
}
}
/**
* 密码显示/隐藏切换处理(用于模态框中的密码输入框)
* @param {HTMLElement} button - 按钮元素
*/
function handleProviderPasswordToggle(button) {
const targetKey = button.getAttribute('data-target');
const input = button.parentNode.querySelector(`input[data-config-key="${targetKey}"]`);
const icon = button.querySelector('i');
if (!input || !icon) return;
if (input.type === 'password') {
input.type = 'text';
icon.className = 'fas fa-eye-slash';
} else {
input.type = 'password';
icon.className = 'fas fa-eye';
}
}
// 数据加载函数(需要从主模块导入)
let loadInitialData;
let saveConfiguration;
let reloadConfig;
// 刷新处理函数
async function handleRefresh() {
try {
// 先刷新基础数据
if (loadInitialData) {
loadInitialData();
}
// 如果reloadConfig函数可用则也刷新配置
if (reloadConfig) {
await reloadConfig();
}
} catch (error) {
console.error('刷新失败:', error);
showToast(t('common.error'), t('common.refresh.failed') + ': ' + error.message, 'error');
}
}
export function setDataLoaders(dataLoader, configSaver) {
loadInitialData = dataLoader;
saveConfiguration = configSaver;
}
export function setReloadConfig(configReloader) {
reloadConfig = configReloader;
}
export {
initEventListeners,
handleProviderChange,
handleGeminiCredsTypeChange,
handleKiroCredsTypeChange,
handlePasswordToggle,
handleProviderPoolsConfigChange,
handleProviderPasswordToggle
};