caddydash/frontend/js/app.js
2025-06-30 15:31:14 +08:00

268 lines
No EOL
11 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.

// 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();