// js/ui.js - 管理所有与UI渲染和DOM操作相关的函数 export const DOMElements = { sidebar: document.getElementById('sidebar'), menuToggleBtn: document.getElementById('menu-toggle-btn'), mainContent: document.querySelector('.main-content'), configListPanel: document.getElementById('config-list-panel'), configFormPanel: document.getElementById('config-form-panel'), renderedOutputPanel: document.getElementById('rendered-output-panel'), configForm: document.getElementById('config-form'), formTitle: document.getElementById('form-title'), backToListBtn: document.getElementById('back-to-list-btn'), domainInput: document.getElementById('domain'), originalFilenameInput: document.getElementById('original-filename'), headersContainer: document.getElementById('headers-container'), addNewConfigBtn: document.getElementById('add-new-config-btn'), cancelEditBtn: document.getElementById('cancel-edit-btn'), configListContainer: document.getElementById('config-list'), renderedContentCode: document.getElementById('rendered-content'), toastContainer: document.getElementById('toast-container'), dialogContainer: document.getElementById('dialog-container'), modalContainer: document.getElementById('modal-container'), themeToggleInput: document.getElementById('theme-toggle-input'), caddyStatusIndicator: document.getElementById('caddy-status-indicator'), caddyActionButtonContainer: document.getElementById('caddy-action-button-container'), logoutBtn: document.getElementById('logout-btn'), serviceModeControl: document.getElementById('service-mode-control'), upstreamFieldset: document.getElementById('upstream-fieldset'), fileserverFieldset: document.getElementById('fileserver-fieldset'), upstreamHeadersContainer: document.getElementById('upstream-headers-container'), mutiUpstreamCheckbox: document.getElementById('muti_upstream'), singleUpstreamGroup: document.getElementById('single-upstream-group'), multiUpstreamGroup: document.getElementById('multi-upstream-group'), multiUpstreamContainer: document.getElementById('multi-upstream-container'), addMultiUpstreamBtn: document.getElementById('add-multi-upstream-btn'), }; export function switchView(viewToShow) { [DOMElements.configListPanel, DOMElements.configFormPanel, DOMElements.renderedOutputPanel] .forEach(view => view.classList.add('hidden')); if (viewToShow) viewToShow.classList.remove('hidden'); DOMElements.addNewConfigBtn.disabled = (viewToShow === DOMElements.configFormPanel); } export function renderConfigList(filenames) { DOMElements.configListContainer.innerHTML = ''; if (!filenames || filenames.length === 0) { DOMElements.configListContainer.innerHTML = '

还没有任何配置,请创建一个。

'; return; } filenames.forEach(filename => { const item = document.createElement('li'); item.className = 'config-item'; item.dataset.filename = filename; item.innerHTML = `${filename}
`; DOMElements.configListContainer.appendChild(item); }); } export function addKeyValueInput(container, keyName, valueName, key = '', value = '') { const div = document.createElement('div'); div.className = 'header-entry'; div.innerHTML = ` `; container.appendChild(div); } export function addSingleInput(container, inputName, placeholder, value = '') { const div = document.createElement('div'); div.className = 'header-entry'; div.style.gridTemplateColumns = '1fr auto'; div.innerHTML = ` `; container.appendChild(div); } export function fillForm(config, originalFilename) { DOMElements.originalFilenameInput.value = originalFilename; DOMElements.domainInput.value = config.domain_config?.domain || ''; const enableUpstream = config.upstream_config?.enable_upstream || false; const enableFileServer = config.file_server_config?.enable_file_server || false; let mode = 'none'; if (enableUpstream) mode = 'reverse_proxy'; else if (enableFileServer) mode = 'file_server'; const activeButton = DOMElements.serviceModeControl.querySelector(`[data-mode="${mode}"]`); if (activeButton) updateSegmentedControl(activeButton); const upstreamConfig = config.upstream_config || {}; DOMElements.mutiUpstreamCheckbox.checked = upstreamConfig.muti_upstream || false; document.getElementById('upstream').value = upstreamConfig.upstream || ''; DOMElements.multiUpstreamContainer.innerHTML = ''; if (upstreamConfig.muti_upstream && upstreamConfig.upstream_servers) { upstreamConfig.upstream_servers.forEach(server => { addSingleInput(DOMElements.multiUpstreamContainer, 'upstream_servers', '例如: 127.0.0.1:8081', server); }); } DOMElements.upstreamHeadersContainer.innerHTML = ''; if (upstreamConfig.upstream_headers) { Object.entries(upstreamConfig.upstream_headers).forEach(([k, v]) => v.forEach(val => addKeyValueInput(DOMElements.upstreamHeadersContainer, 'upstream_header_key', 'upstream_header_value', k, val))); } document.getElementById('file_dir_path').value = config.file_server_config?.file_dir_path || ''; document.getElementById('enable_browser').checked = config.file_server_config?.enable_browser || false; DOMElements.headersContainer.innerHTML = ''; if (config.headers) Object.entries(config.headers).forEach(([k, v]) => v.forEach(val => addKeyValueInput(DOMElements.headersContainer, 'header_key', 'header_value', k, val))); document.getElementById('enable_log').checked = config.log_config?.enable_log || false; document.getElementById('enable_error_page').checked = config.error_page_config?.enable_error_page || false; document.getElementById('enable_encode').checked = config.encode_config?.enable_encode || false; updateMultiUpstreamView(DOMElements.mutiUpstreamCheckbox.checked); } export function showRenderedConfig(configs, filename) { const targetConfig = configs.find(c => c.filename === filename); if (targetConfig && targetConfig.rendered_content) { DOMElements.renderedContentCode.textContent = atob(targetConfig.rendered_content); DOMElements.renderedOutputPanel.classList.remove('hidden'); } else { DOMElements.renderedOutputPanel.classList.add('hidden'); } } function createButton(text, className, onClick) { const button = document.createElement('button'); button.className = `btn ${className}`; button.innerHTML = `${text}`; button.addEventListener('click', onClick); return button; } export function updateCaddyStatusView(status, handlers) { const { handleReloadCaddy, handleStopCaddy, handleStartCaddy } = handlers; const dot = DOMElements.caddyStatusIndicator.querySelector('.status-dot'); const text = DOMElements.caddyStatusIndicator.querySelector('.status-text'); const buttonContainer = DOMElements.caddyActionButtonContainer; dot.className = 'status-dot'; buttonContainer.innerHTML = ''; let statusText, dotClass; switch (status) { case 'running': statusText = '运行中'; dotClass = 'running'; buttonContainer.appendChild(createButton('重载配置', 'btn-warning', handleReloadCaddy)); buttonContainer.appendChild(createButton('停止 Caddy', 'btn-danger', handleStopCaddy)); break; case 'stopped': statusText = '已停止'; dotClass = 'stopped'; buttonContainer.appendChild(createButton('启动 Caddy', 'btn-success', handleStartCaddy)); break; case 'checking': statusText = '检查中...'; dotClass = 'checking'; break; default: statusText = '状态未知'; dotClass = 'error'; break; } text.textContent = statusText; dot.classList.add(dotClass); } export function updateServiceModeView(mode) { DOMElements.upstreamFieldset.classList.toggle('hidden', mode !== 'reverse_proxy'); DOMElements.fileserverFieldset.classList.toggle('hidden', mode !== 'file_server'); } export function updateMultiUpstreamView(isMulti) { DOMElements.singleUpstreamGroup.classList.toggle('hidden', isMulti); DOMElements.multiUpstreamGroup.classList.toggle('hidden', !isMulti); } export function updateSegmentedControl(activeButton) { const slider = document.getElementById('segmented-control-slider'); const control = DOMElements.serviceModeControl; if (!activeButton || !slider || !control) return; control.querySelectorAll('button').forEach(btn => btn.classList.remove('active')); activeButton.classList.add('active'); slider.style.width = `${activeButton.offsetWidth}px`; slider.style.transform = `translateX(${activeButton.offsetLeft}px)`; } // 彻底重构: 使用 Promise 并管理自己的事件监听器生命周期 export function createPresetSelectionModal(presets) { return new Promise(resolve => { const modalContainer = DOMElements.modalContainer; if (!modalContainer) return resolve(null); // 安全退出 const presetItems = presets.map(p => `
  • ${p.name}

    ${p.description}

  • `).join(''); const modalHTML = ` `; modalContainer.innerHTML = modalHTML; requestAnimationFrame(() => modalContainer.classList.add('active')); const cleanupAndResolve = (value) => { modalContainer.removeEventListener('click', eventHandler); modalContainer.classList.remove('active'); setTimeout(() => { modalContainer.innerHTML = ''; resolve(value); }, 300); }; const eventHandler = (e) => { if (e.target.classList.contains('modal-overlay') || e.target.closest('[data-modal-close]')) { cleanupAndResolve(null); // 用户取消 } const listItem = e.target.closest('li[data-preset-id]'); if (listItem) { cleanupAndResolve(listItem.dataset.presetId); // 用户选择 } }; modalContainer.addEventListener('click', eventHandler); }); } export function createCustomSelect(containerId, options, onSelect) { const container = document.getElementById(containerId); if (!container) return; // 从容器的ID动态生成隐藏input的name属性 // 例如, id 'select-log-level' -> name 'log_level' const inputName = container.id.replace('select-', '').replace(/-/g, '_'); container.innerHTML = `
    `; const selectedDiv = container.querySelector('.select-selected'); const itemsDiv = container.querySelector('.select-items'); const hiddenInput = container.querySelector('input[type="hidden"]'); itemsDiv.innerHTML = ''; if (!options || options.length === 0) { selectedDiv.textContent = '无可用选项'; return; } options.forEach((option, index) => { const item = document.createElement('div'); item.textContent = option; item.dataset.value = option; if (index === 0) { selectedDiv.textContent = option; hiddenInput.value = option; } item.addEventListener('click', function(e) { selectedDiv.textContent = this.textContent; hiddenInput.value = this.dataset.value; itemsDiv.classList.remove('select-show'); selectedDiv.classList.remove('select-arrow-active'); onSelect && onSelect(this.dataset.value); e.stopPropagation(); }); itemsDiv.appendChild(item); }); selectedDiv.addEventListener('click', (e) => { e.stopPropagation(); document.querySelectorAll('.select-items.select-show').forEach(openSelect => { if (openSelect !== itemsDiv) { openSelect.classList.remove('select-show'); openSelect.previousElementSibling.classList.remove('select-arrow-active'); } }); itemsDiv.classList.toggle('select-show'); selectedDiv.classList.toggle('select-arrow-active'); }); document.addEventListener('click', () => { itemsDiv.classList.remove('select-show'); if(selectedDiv) selectedDiv.classList.remove('select-arrow-active'); }); }