// js/app.js - 主应用入口 import { state } from './state.js'; import { api } from './api.js'; import { theme, activateNav } from './common.js'; import { notification } from './notifications.js'; import { initCaddyStatus } from './caddy.js'; import { DOMElements, switchView, renderConfigList, addKeyValueInput, addSingleInput, fillForm, showRenderedConfig, updateCaddyStatusView, updateSegmentedControl, updateServiceModeView, updateMultiUpstreamView, createPresetSelectionModal } from './ui.js'; const POLLING_INTERVAL = 5000; let caddyStatusInterval; function getFormStateAsString() { const formData = new FormData(DOMElements.configForm); const data = {}; const activeModeButton = DOMElements.serviceModeControl.querySelector('button.active'); data.active_mode = activeModeButton ? activeModeButton.dataset.mode : 'none'; for (const [key, value] of formData.entries()) { const el = DOMElements.configForm.querySelector(`[name="${key}"]`); if (el?.type === 'checkbox') data[key] = el.checked; else if (data[key]) { if (!Array.isArray(data[key])) data[key] = [data[key]]; data[key].push(value); } else data[key] = value; } return JSON.stringify(data); } async function attemptExitForm() { if (getFormStateAsString() !== state.initialFormState) { if (await notification.confirm('您有未保存的更改。确定要放弃吗?')) switchView(DOMElements.configListPanel); } else switchView(DOMElements.configListPanel); } async function handleLogout() { if (!await notification.confirm('您确定要退出登录吗?')) return; notification.toast('正在退出...', 'info'); setTimeout(() => { window.location.href = `/v0/api/auth/logout`; }, 500); } async function loadAllConfigs() { try { const filenames = await api.get('/config/filenames'); renderConfigList(filenames); } catch (error) { if (error.message) notification.toast(`加载配置列表失败: ${error.message}`, 'error'); } } async function handleEditConfig(originalFilename) { try { const [config, rendered] = await Promise.all([ api.get(`/config/file/${originalFilename}`), api.get('/config/files/rendered') ]); state.isEditing = true; switchView(DOMElements.configFormPanel); DOMElements.formTitle.textContent = '编辑配置'; fillForm(config, originalFilename); showRenderedConfig(rendered, originalFilename); const mode = config.upstream_config?.enable_upstream ? 'reverse_proxy' : (config.file_server_config?.enable_file_server ? 'file_server' : 'none'); updateServiceModeView(mode); state.initialFormState = getFormStateAsString(); } catch (error) { notification.toast(`加载配置详情失败: ${error.message}`, 'error'); } } async function handleDeleteConfig(filename) { if (!await notification.confirm(`确定要删除配置 "${filename}" 吗?`)) return; try { await api.delete(`/config/file/${filename}`); notification.toast('配置已成功删除。', 'success'); loadAllConfigs(); } catch (error) { notification.toast(`删除失败: ${error.message}`, 'error'); } } async function handleSaveConfig(e) { e.preventDefault(); const formData = new FormData(DOMElements.configForm); const domain = formData.get('domain'); if (!domain) { notification.toast('域名不能为空。', 'error'); return; } const getHeadersMap = (keyName, valueName) => { const headers = {}; formData.getAll(keyName).forEach((key, i) => { if (key) { if (!headers[key]) headers[key] = []; headers[key].push(formData.getAll(valueName)[i]); } }); return headers; }; const activeModeButton = DOMElements.serviceModeControl.querySelector('button.active'); const activeMode = activeModeButton ? activeModeButton.dataset.mode : 'none'; const isMutiUpstream = DOMElements.mutiUpstreamCheckbox.checked; const configData = { mode: "uni", domain_config: { domain: domain, muti_domains: false, domains: [] }, upstream_config: { enable_upstream: activeMode === 'reverse_proxy', upstream: formData.get('upstream'), muti_upstream: isMutiUpstream, upstream_servers: formData.getAll('upstream_servers').filter(s => s), upstream_headers: getHeadersMap('upstream_header_key', 'upstream_header_value'), }, file_server_config: { enable_file_server: activeMode === 'file_server', file_dir_path: formData.get('file_dir_path'), enable_browser: DOMElements.configForm.querySelector('#enable_browser').checked }, headers: getHeadersMap('header_key', 'header_value'), log_config: { enable_log: DOMElements.configForm.querySelector('#enable_log').checked, log_domain: domain }, error_page_config: { enable_error_page: DOMElements.configForm.querySelector('#enable_error_page').checked }, encode_config: { enable_encode: DOMElements.configForm.querySelector('#enable_encode').checked } }; try { const result = await api.put(`/config/file/${domain}`, configData); state.isEditing = false; notification.toast(result.message || '配置已成功保存。', 'success'); setTimeout(() => { switchView(DOMElements.configListPanel); loadAllConfigs(); }, 500); } catch (error) { notification.toast(`保存失败: ${error.message}`, 'error'); } } async function openPresetModal(targetType) { const applicablePresets = state.headerPresets.filter(p => p.target === targetType || p.target === 'any'); if (applicablePresets.length === 0) { notification.toast(`没有适用于此区域的预设。`, 'info'); return; } const presetId = await createPresetSelectionModal(applicablePresets); if (!presetId) return; // 用户取消了选择 let targetContainer, keyName, valueName; if (targetType === 'global') { targetContainer = DOMElements.headersContainer; keyName = 'header_key'; valueName = 'header_value'; } else if (targetType === 'upstream') { targetContainer = DOMElements.upstreamHeadersContainer; keyName = 'upstream_header_key'; valueName = 'upstream_header_value'; } else { return; } // 在获取到presetId后,再执行填充逻辑 try { notification.toast('正在加载预设...', 'info', 1000); const preset = await api.get(`/config/headers-presets/${presetId}`); if (!preset.headers) { notification.toast('此预设无数据。', 'info'); return; } const choice = await notification.confirm('如何填充预设?', '选择填充方式', { confirmText: '追加', cancelText: '替换' }); if (choice === false) { // 用户选择了“替换” targetContainer.innerHTML = ''; } Object.entries(preset.headers).forEach(([key, values]) => { values.forEach(value => { addKeyValueInput(targetContainer, keyName, valueName, key, value); }); }); notification.toast(`已成功填充预设 "${preset.name}"`, 'success'); } catch (error) { notification.toast(`加载预设失败: ${error.message}`, 'error'); } } function init() { theme.init(DOMElements.themeToggleInput); notification.init(DOMElements.toastContainer, DOMElements.dialogContainer, DOMElements.modalContainer); activateNav('configs'); initCaddyStatus(); loadAllConfigs(); api.get('/config/headers-presets') .then(presets => { state.headerPresets = presets || []; }) .catch(err => { if (err.message) notification.toast(`加载Header预设失败: ${err.message}`, 'error'); }); DOMElements.menuToggleBtn.addEventListener('click', () => DOMElements.sidebar.classList.toggle('is-open')); DOMElements.mainContent.addEventListener('click', () => DOMElements.sidebar.classList.remove('is-open')); DOMElements.addNewConfigBtn.addEventListener('click', () => { state.isEditing = false; switchView(DOMElements.configFormPanel); DOMElements.formTitle.textContent = '创建新配置'; DOMElements.configForm.reset(); const noneButton = DOMElements.serviceModeControl.querySelector('[data-mode="none"]'); if (noneButton) updateSegmentedControl(noneButton); updateServiceModeView('none'); updateMultiUpstreamView(false); state.initialFormState = getFormStateAsString(); DOMElements.headersContainer.innerHTML = ''; DOMElements.upstreamHeadersContainer.innerHTML = ''; DOMElements.multiUpstreamContainer.innerHTML = ''; DOMElements.originalFilenameInput.value = ''; }); DOMElements.backToListBtn.addEventListener('click', attemptExitForm); DOMElements.cancelEditBtn.addEventListener('click', attemptExitForm); DOMElements.configForm.addEventListener('submit', handleSaveConfig); DOMElements.logoutBtn.addEventListener('click', handleLogout); DOMElements.configListContainer.addEventListener('click', e => { const button = e.target.closest('button'); if (!button) return; const filename = button.closest('.config-item').dataset.filename; if (button.classList.contains('edit-btn')) handleEditConfig(filename); if (button.classList.contains('delete-btn')) handleDeleteConfig(filename); }); DOMElements.configForm.querySelector('button[data-add-target="global"]').addEventListener('click', (e) => { e.preventDefault(); addKeyValueInput(DOMElements.headersContainer, 'header_key', 'header_value'); }); DOMElements.configForm.querySelector('button[data-add-target="upstream"]').addEventListener('click', (e) => { e.preventDefault(); addKeyValueInput(DOMElements.upstreamHeadersContainer, 'upstream_header_key', 'upstream_header_value'); }); DOMElements.configForm.querySelector('button[data-preset-target="global"]').addEventListener('click', (e) => { e.preventDefault(); openPresetModal('global'); }); DOMElements.configForm.querySelector('button[data-preset-target="upstream"]').addEventListener('click', (e) => { e.preventDefault(); openPresetModal('upstream'); }); DOMElements.addMultiUpstreamBtn.addEventListener('click', (e) => { e.preventDefault(); addSingleInput(DOMElements.multiUpstreamContainer, 'upstream_servers', '例如: 127.0.0.1:8081'); }); DOMElements.serviceModeControl.addEventListener('click', (e) => { const button = e.target.closest('button'); if (button) { e.preventDefault(); const mode = button.dataset.mode; updateSegmentedControl(button); updateServiceModeView(mode); } }); DOMElements.mutiUpstreamCheckbox.addEventListener('change', (e) => { updateMultiUpstreamView(e.target.checked); }); } init();