add multupstream && new select

This commit is contained in:
wjqserver 2025-06-20 17:04:14 +08:00
parent b10790c212
commit 30da719d57
4 changed files with 218 additions and 163 deletions

View file

@ -37,8 +37,6 @@ body {
transition: background-color var(--transition-speed), color var(--transition-speed); transition: background-color var(--transition-speed), color var(--transition-speed);
} }
.hidden { display: none !important; } .hidden { display: none !important; }
/* --- 登录页 --- */
.login-page-body { .login-page-body {
display: flex; display: flex;
align-items: center; align-items: center;
@ -56,7 +54,7 @@ body {
text-align: center; text-align: center;
} }
.login-header { margin-bottom: 32px; } .login-header { margin-bottom: 32px; }
.login-header .fa-rocket { .login-header .fa-rocket, .login-header .fa-magic-wand-sparkles {
font-size: 2.5rem; font-size: 2.5rem;
color: var(--primary-color); color: var(--primary-color);
margin-bottom: 16px; margin-bottom: 16px;
@ -71,10 +69,6 @@ body {
text-align: left; text-align: left;
margin-bottom: 20px; margin-bottom: 20px;
} }
#init-form .form-group {
text-align: left;
margin-bottom: 20px;
}
.btn-login { .btn-login {
margin-top: 16px; margin-top: 16px;
width: 100%; width: 100%;
@ -82,16 +76,6 @@ body {
padding-left: 24px; padding-left: 24px;
padding-right: 24px; padding-right: 24px;
} }
.error-text {
color: var(--danger-color);
font-size: 0.875rem;
text-align: left;
margin-top: -12px;
margin-bottom: 16px;
min-height: 1.2em;
}
/* --- Toast 通知 --- */
.toast-container { .toast-container {
position: fixed; position: fixed;
top: 20px; top: 20px;
@ -125,8 +109,6 @@ body {
.toast.success .toast-icon { color: var(--success-color); } .toast.success .toast-icon { color: var(--success-color); }
.toast.error .toast-icon { color: var(--danger-color); } .toast.error .toast-icon { color: var(--danger-color); }
.toast.info .toast-icon { color: var(--primary-color); } .toast.info .toast-icon { color: var(--primary-color); }
/* --- 交互式对话框/Modal --- */
#dialog-container { #dialog-container {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
z-index: 1000; display: flex; align-items: center; justify-content: center; z-index: 1000; display: flex; align-items: center; justify-content: center;
@ -150,8 +132,6 @@ body {
.dialog-message { margin: 16px 0 24px; font-size: 1rem; color: var(--text-color-secondary); } .dialog-message { margin: 16px 0 24px; font-size: 1rem; color: var(--text-color-secondary); }
.dialog-actions { display: flex; justify-content: center; gap: 12px; } .dialog-actions { display: flex; justify-content: center; gap: 12px; }
.dialog-actions .btn { width: auto; } .dialog-actions .btn { width: auto; }
/* --- 主应用布局 --- */
.app-container { display: flex; height: 100vh; } .app-container { display: flex; height: 100vh; }
.main-content { flex-grow: 1; padding: 24px 32px; overflow-y: auto; } .main-content { flex-grow: 1; padding: 24px 32px; overflow-y: auto; }
#view-container { position: relative; } #view-container { position: relative; }
@ -364,34 +344,46 @@ fieldset { border: none; padding: 0; margin: 0 0 24px 0; }
width: 100%; padding: 10px; background-color: var(--bg-color); border: 1px solid var(--border-color); width: 100%; padding: 10px; background-color: var(--bg-color); border: 1px solid var(--border-color);
border-radius: var(--border-radius-small); color: var(--text-color); border-radius: var(--border-radius-small); color: var(--text-color);
} }
.custom-select { position: relative; } .segmented-control {
.select-selected { position: relative;
display: flex; justify-content: space-between; align-items: center; display: flex;
padding: 12px; background-color: var(--surface-color); border: 1px solid var(--border-color); width: 100%;
border-radius: var(--border-radius-small); cursor: pointer; user-select: none; background-color: var(--bg-color);
transition: border-color 0.2s, box-shadow 0.2s, background-color var(--transition-speed); border: 1px solid var(--border-color);
border-radius: var(--border-radius-small);
padding: 4px;
transition: background-color var(--transition-speed), border-color var(--transition-speed);
} }
.select-selected.select-arrow-active { .segmented-control button {
border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2); flex: 1;
padding: 8px 12px;
border: none;
background-color: transparent;
color: var(--text-color-secondary);
font-weight: 600;
cursor: pointer;
transition: color 0.2s ease;
z-index: 2;
} }
.select-selected::after { .segmented-control button:hover {
content: '\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; color: var(--text-color);
transition: transform var(--transition-speed) ease;
} }
.select-selected.select-arrow-active::after { transform: rotate(180deg); } .segmented-control button.active {
.select-items { color: white;
position: absolute; background-color: var(--surface-color); top: 100%; left: 0; right: 0;
border: 1px solid var(--border-color); border-radius: var(--border-radius-small);
box-shadow: 0 8px 16px rgba(0,0,0,0.1); z-index: 99;
margin-top: 8px; max-height: 200px; overflow-y: auto;
opacity: 0; transform: translateY(-10px); visibility: hidden;
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
} }
.select-items.select-show { opacity: 1; transform: translateY(0); visibility: visible; } [data-theme="dark"] .segmented-control button.active {
.select-items div { color: var(--text-color);
padding: 12px 16px; cursor: pointer; transition: background-color 0.2s; }
#segmented-control-slider {
position: absolute;
top: 4px;
bottom: 4px;
background-color: var(--primary-color);
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1;
} }
.select-items div:hover, .same-as-selected { background-color: var(--bg-color); }
#rendered-output-panel pre { #rendered-output-panel pre {
background-color: var(--bg-color); padding: 16px; border-radius: var(--border-radius-small); background-color: var(--bg-color); padding: 16px; border-radius: var(--border-radius-small);
overflow-x: auto; white-space: pre-wrap; word-break: break-all; overflow-x: auto; white-space: pre-wrap; word-break: break-all;

View file

@ -69,38 +69,57 @@
<form id="config-form"> <form id="config-form">
<input type="hidden" id="original-filename" value=""> <input type="hidden" id="original-filename" value="">
<div class="form-grid"> <fieldset>
<legend>基础配置</legend>
<div class="form-group"> <div class="form-group">
<label for="domain">域名 (将用作文件名)</label> <label for="domain">域名 (将用作文件名)</label>
<input type="text" id="domain" name="domain" required> <input type="text" id="domain" name="domain" required>
</div> </div>
<div class="form-group">
<label>模板类型</label>
<div id="custom-select-tmpl" class="custom-select"></div>
</div>
</div>
<!-- 反向代理配置 -->
<fieldset id="upstream-fieldset" class="hidden">
<legend>反向代理 (Upstream)</legend>
<div class="form-group">
<label for="upstream">上游服务地址</label>
<input type="text" id="upstream" name="upstream" placeholder="例如: 127.0.0.1:8080">
</div>
<!-- 新增: 上游请求头配置区域 -->
<div class="sub-fieldset">
<p class="sub-legend">上游请求头 (Upstream Headers)</p>
<div id="upstream-headers-container"></div>
<button type="button" id="add-upstream-header-btn" class="btn btn-secondary btn-small">
<i class="fa-solid fa-plus"></i> 添加上游请求头
</button>
</div>
</fieldset> </fieldset>
<!-- 文件服务器配置 --> <fieldset>
<legend>服务模式</legend>
<div id="service-mode-control" class="segmented-control">
<div id="segmented-control-slider"></div>
<button type="button" data-mode="none" class="active"></button>
<button type="button" data-mode="reverse_proxy">反向代理</button>
<button type="button" data-mode="file_server">文件服务</button>
</div>
</fieldset>
<fieldset id="upstream-fieldset" class="hidden">
<legend>反向代理配置</legend>
<!-- 单上游输入框, 在多上游模式下隐藏 -->
<div class="form-group" id="single-upstream-group">
<label for="upstream">上游服务地址</label>
<input type="text" id="upstream" name="upstream" placeholder="例如: 127.0.0.1:8080">
</div>
<!-- 多上游配置区域 -->
<div class="sub-fieldset">
<label class="custom-checkbox">
<input type="checkbox" id="muti_upstream" name="muti_upstream">
<span class="checkmark"></span> 启用多上游负载均衡
</label>
<div id="multi-upstream-group" class="hidden" style="margin-top: 16px;">
<p class="sub-legend">上游服务器列表</p>
<div id="multi-upstream-container"></div>
<button type="button" id="add-multi-upstream-btn" class="btn btn-secondary btn-small">
<i class="fa-solid fa-plus"></i> 添加上游服务器
</button>
</div>
</div>
<div class="sub-fieldset">
<p class="sub-legend">上游请求头 (Upstream Headers)</p>
<div id="upstream-headers-container"></div>
<button type="button" id="add-upstream-header-btn" class="btn btn-secondary btn-small"><i class="fa-solid fa-plus"></i> 添加上游请求头</button>
</div>
</fieldset>
<fieldset id="fileserver-fieldset" class="hidden"> <fieldset id="fileserver-fieldset" class="hidden">
<legend>文件服务 (File Server)</legend> <legend>文件服务配置</legend>
<div class="form-group"> <div class="form-group">
<label for="file_dir_path">根目录路径</label> <label for="file_dir_path">根目录路径</label>
<input type="text" id="file_dir_path" name="file_dir_path" placeholder="例如: /srv/www"> <input type="text" id="file_dir_path" name="file_dir_path" placeholder="例如: /srv/www">
@ -111,7 +130,6 @@
</label> </label>
</fieldset> </fieldset>
<!-- 全局请求头 -->
<fieldset id="headers-fieldset"> <fieldset id="headers-fieldset">
<legend>全局请求头 (Headers)</legend> <legend>全局请求头 (Headers)</legend>
<div id="headers-container"></div> <div id="headers-container"></div>
@ -119,7 +137,7 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>功能开关</legend> <legend>附加功能</legend>
<div class="checkbox-grid"> <div class="checkbox-grid">
<label class="custom-checkbox"><input type="checkbox" id="enable_log" name="enable_log"><span class="checkmark"></span> 启用日志</label> <label class="custom-checkbox"><input type="checkbox" id="enable_log" name="enable_log"><span class="checkmark"></span> 启用日志</label>
<label class="custom-checkbox"><input type="checkbox" id="enable_error_page" name="enable_error_page"><span class="checkmark"></span> 启用错误页</label> <label class="custom-checkbox"><input type="checkbox" id="enable_error_page" name="enable_error_page"><span class="checkmark"></span> 启用错误页</label>

View file

@ -4,7 +4,7 @@ import { state } from './state.js';
import { api } from './api.js'; import { api } from './api.js';
import { initTheme } from './theme.js'; import { initTheme } from './theme.js';
import { notification } from './notifications.js'; import { notification } from './notifications.js';
import { DOMElements, switchView, renderConfigList, createCustomSelect, addKeyValueInput, fillForm, showRenderedConfig, updateCaddyStatusView, updateFormVisibility } from './ui.js'; import { DOMElements, switchView, renderConfigList, addKeyValueInput, addSingleInput, fillForm, showRenderedConfig, updateCaddyStatusView, updateSegmentedControl, updateServiceModeView, updateMultiUpstreamView } from './ui.js';
const POLLING_INTERVAL = 5000; const POLLING_INTERVAL = 5000;
let caddyStatusInterval; let caddyStatusInterval;
@ -12,6 +12,8 @@ let caddyStatusInterval;
function getFormStateAsString() { function getFormStateAsString() {
const formData = new FormData(DOMElements.configForm); const formData = new FormData(DOMElements.configForm);
const data = {}; const data = {};
const activeModeButton = DOMElements.serviceModeControl.querySelector('button.active');
data.active_mode = activeModeButton ? activeModeButton.dataset.mode : 'none';
for (const [key, value] of formData.entries()) { for (const [key, value] of formData.entries()) {
const el = DOMElements.configForm.querySelector(`[name="${key}"]`); const el = DOMElements.configForm.querySelector(`[name="${key}"]`);
if (el?.type === 'checkbox') data[key] = el.checked; if (el?.type === 'checkbox') data[key] = el.checked;
@ -79,7 +81,8 @@ async function handleEditConfig(originalFilename) {
DOMElements.formTitle.textContent = '编辑配置'; DOMElements.formTitle.textContent = '编辑配置';
fillForm(config, originalFilename); fillForm(config, originalFilename);
showRenderedConfig(rendered, originalFilename); showRenderedConfig(rendered, originalFilename);
updateFormVisibility(config.tmpl_type, state.availableTemplates); const mode = config.upstream_config?.enable_upstream ? 'reverse_proxy' : (config.file_server_config?.enable_file_server ? 'file_server' : 'none');
updateServiceModeView(mode);
state.initialFormState = getFormStateAsString(); state.initialFormState = getFormStateAsString();
} catch(error) { notification.toast(`加载配置详情失败: ${error.message}`, 'error'); } } catch(error) { notification.toast(`加载配置详情失败: ${error.message}`, 'error'); }
} }
@ -94,8 +97,8 @@ async function handleDeleteConfig(filename) {
async function handleSaveConfig(e) { async function handleSaveConfig(e) {
e.preventDefault(); e.preventDefault();
const formData = new FormData(DOMElements.configForm); const formData = new FormData(DOMElements.configForm);
const filename = formData.get('domain'); const domain = formData.get('domain');
if (!filename) { if (!domain) {
notification.toast('域名不能为空。', 'error'); notification.toast('域名不能为空。', 'error');
return; return;
} }
@ -109,20 +112,32 @@ async function handleSaveConfig(e) {
}); });
return headers; return headers;
}; };
const globalHeaders = getHeadersMap('header_key', 'header_value'); const activeModeButton = DOMElements.serviceModeControl.querySelector('button.active');
const upstreamHeaders = getHeadersMap('upstream_header_key', 'upstream_header_value'); const activeMode = activeModeButton ? activeModeButton.dataset.mode : 'none';
const isMutiUpstream = DOMElements.mutiUpstreamCheckbox.checked;
const configData = { const configData = {
domain: filename, mode: "uni",
tmpl_type: formData.get('tmpl_type'), domain_config: { domain: domain, muti_domains: false, domains: [] },
upstream: { upstream: formData.get('upstream'), muti_upstream: false, upstreams: [], upstream_headers: upstreamHeaders }, upstream_config: {
file_server: { file_dir_path: formData.get('file_dir_path'), enable_browser: DOMElements.configForm.querySelector('#enable_browser').checked }, enable_upstream: activeMode === 'reverse_proxy',
headers: globalHeaders, upstream: formData.get('upstream'),
log: { enable_log: DOMElements.configForm.querySelector('#enable_log').checked, log_domain: filename }, muti_upstream: isMutiUpstream,
error_page: { enable_error_page: DOMElements.configForm.querySelector('#enable_error_page').checked }, upstream_servers: formData.getAll('upstream_servers').filter(s => s),
encode: { enable_encode: DOMElements.configForm.querySelector('#enable_encode').checked } 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 { try {
const result = await api.put(`/config/file/${filename}`, configData); const result = await api.put(`/config/file/${domain}`, configData);
state.isEditing = false; state.isEditing = false;
notification.toast(result.message || '配置已成功保存。', 'success'); notification.toast(result.message || '配置已成功保存。', 'success');
setTimeout(() => { setTimeout(() => {
@ -131,46 +146,45 @@ async function handleSaveConfig(e) {
}, 500); }, 500);
} catch(error) { notification.toast(`保存失败: ${error.message}`, 'error'); } } catch(error) { notification.toast(`保存失败: ${error.message}`, 'error'); }
} }
function init() { function init() {
initTheme(DOMElements.themeToggleInput); initTheme(DOMElements.themeToggleInput);
notification.init(DOMElements.toastContainer, DOMElements.dialogContainer); notification.init(DOMElements.toastContainer, DOMElements.dialogContainer);
loadAllConfigs(); loadAllConfigs();
api.get('/config/templates')
.then(templates => {
state.availableTemplates = templates || [];
const options = state.availableTemplates.length > 0 ? state.availableTemplates : ['无可用模板'];
createCustomSelect('custom-select-tmpl', options, (selectedValue) => {
updateFormVisibility(selectedValue, state.availableTemplates);
});
if (options[0] !== '无可用模板') updateFormVisibility(options[0], state.availableTemplates);
})
.catch(err => { if(err.message) notification.toast(`加载模板失败: ${err.message}`, 'error') });
checkCaddyStatus(); checkCaddyStatus();
caddyStatusInterval = setInterval(checkCaddyStatus, POLLING_INTERVAL); caddyStatusInterval = setInterval(checkCaddyStatus, POLLING_INTERVAL);
DOMElements.menuToggleBtn.addEventListener('click', () => DOMElements.sidebar.classList.toggle('is-open')); DOMElements.menuToggleBtn.addEventListener('click', () => DOMElements.sidebar.classList.toggle('is-open'));
DOMElements.mainContent.addEventListener('click', () => DOMElements.sidebar.classList.remove('is-open')); DOMElements.mainContent.addEventListener('click', () => DOMElements.sidebar.classList.remove('is-open'));
DOMElements.addNewConfigBtn.addEventListener('click', () => { DOMElements.addNewConfigBtn.addEventListener('click', () => {
state.isEditing = false; state.isEditing = false;
switchView(DOMElements.configFormPanel); switchView(DOMElements.configFormPanel);
DOMElements.formTitle.textContent = '创建新配置'; DOMElements.formTitle.textContent = '创建新配置';
DOMElements.configForm.reset(); DOMElements.configForm.reset();
if (state.availableTemplates.length > 0) {
const selectContainer = document.getElementById('custom-select-tmpl'); const noneButton = DOMElements.serviceModeControl.querySelector('[data-mode="none"]');
selectContainer.querySelector('.select-selected').textContent = state.availableTemplates[0]; if(noneButton) updateSegmentedControl(noneButton);
selectContainer.querySelector('input[type="hidden"]').value = state.availableTemplates[0]; updateServiceModeView('none');
updateFormVisibility(state.availableTemplates[0], state.availableTemplates); updateMultiUpstreamView(false);
}
state.initialFormState = getFormStateAsString(); state.initialFormState = getFormStateAsString();
DOMElements.headersContainer.innerHTML = ''; DOMElements.headersContainer.innerHTML = '';
DOMElements.upstreamHeadersContainer.innerHTML = ''; DOMElements.upstreamHeadersContainer.innerHTML = '';
DOMElements.multiUpstreamContainer.innerHTML = '';
DOMElements.originalFilenameInput.value = ''; DOMElements.originalFilenameInput.value = '';
}); });
DOMElements.backToListBtn.addEventListener('click', attemptExitForm); DOMElements.backToListBtn.addEventListener('click', attemptExitForm);
DOMElements.cancelEditBtn.addEventListener('click', attemptExitForm); DOMElements.cancelEditBtn.addEventListener('click', attemptExitForm);
DOMElements.addHeaderBtn.addEventListener('click', () => addKeyValueInput(DOMElements.headersContainer, 'header_key', 'header_value')); DOMElements.addHeaderBtn.addEventListener('click', () => addKeyValueInput(DOMElements.headersContainer, 'header_key', 'header_value'));
DOMElements.addUpstreamHeaderBtn.addEventListener('click', () => addKeyValueInput(DOMElements.upstreamHeadersContainer, 'upstream_header_key', 'upstream_header_value')); DOMElements.addUpstreamHeaderBtn.addEventListener('click', () => addKeyValueInput(DOMElements.upstreamHeadersContainer, 'upstream_header_key', 'upstream_header_value'));
DOMElements.addMultiUpstreamBtn.addEventListener('click', () => addSingleInput(DOMElements.multiUpstreamContainer, 'upstream_servers', '例如: 127.0.0.1:8081'));
DOMElements.configForm.addEventListener('submit', handleSaveConfig); DOMElements.configForm.addEventListener('submit', handleSaveConfig);
DOMElements.logoutBtn.addEventListener('click', handleLogout); DOMElements.logoutBtn.addEventListener('click', handleLogout);
DOMElements.configListContainer.addEventListener('click', e => { DOMElements.configListContainer.addEventListener('click', e => {
const button = e.target.closest('button'); const button = e.target.closest('button');
if (!button) return; if (!button) return;
@ -178,5 +192,19 @@ function init() {
if (button.classList.contains('edit-btn')) handleEditConfig(filename); if (button.classList.contains('edit-btn')) handleEditConfig(filename);
if (button.classList.contains('delete-btn')) handleDeleteConfig(filename); if (button.classList.contains('delete-btn')) handleDeleteConfig(filename);
}); });
DOMElements.serviceModeControl.addEventListener('click', (e) => {
const button = e.target.closest('button');
if (button) {
const mode = button.dataset.mode;
updateSegmentedControl(button);
updateServiceModeView(mode);
}
});
DOMElements.mutiUpstreamCheckbox.addEventListener('change', (e) => {
updateMultiUpstreamView(e.target.checked);
});
} }
init(); init();

View file

@ -1,6 +1,5 @@
// js/ui.js - 管理所有与UI渲染和DOM操作相关的函数 // js/ui.js - 管理所有与UI渲染和DOM操作相关的函数
// 集中管理所有需要操作的DOM元素
export const DOMElements = { export const DOMElements = {
sidebar: document.getElementById('sidebar'), sidebar: document.getElementById('sidebar'),
menuToggleBtn: document.getElementById('menu-toggle-btn'), menuToggleBtn: document.getElementById('menu-toggle-btn'),
@ -25,10 +24,16 @@ export const DOMElements = {
caddyStatusIndicator: document.getElementById('caddy-status-indicator'), caddyStatusIndicator: document.getElementById('caddy-status-indicator'),
caddyActionButtonContainer: document.getElementById('caddy-action-button-container'), caddyActionButtonContainer: document.getElementById('caddy-action-button-container'),
logoutBtn: document.getElementById('logout-btn'), logoutBtn: document.getElementById('logout-btn'),
serviceModeControl: document.getElementById('service-mode-control'),
upstreamFieldset: document.getElementById('upstream-fieldset'), upstreamFieldset: document.getElementById('upstream-fieldset'),
fileserverFieldset: document.getElementById('fileserver-fieldset'), fileserverFieldset: document.getElementById('fileserver-fieldset'),
upstreamHeadersContainer: document.getElementById('upstream-headers-container'), upstreamHeadersContainer: document.getElementById('upstream-headers-container'),
addUpstreamHeaderBtn: document.getElementById('add-upstream-header-btn'), addUpstreamHeaderBtn: document.getElementById('add-upstream-header-btn'),
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) { export function switchView(viewToShow) {
@ -53,48 +58,6 @@ export function renderConfigList(filenames) {
}); });
} }
export function createCustomSelect(containerId, options, onSelect) {
const container = document.getElementById(containerId);
container.innerHTML = `<div class="select-selected"></div><div class="select-items"></div><input type="hidden" name="tmpl_type">`;
const selectedDiv = container.querySelector('.select-selected');
const itemsDiv = container.querySelector('.select-items');
const hiddenInput = container.querySelector('input[type="hidden"]');
itemsDiv.innerHTML = '';
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');
selectedDiv.classList.remove('select-arrow-active');
});
}
export function addKeyValueInput(container, keyName, valueName, key = '', value = '') { export function addKeyValueInput(container, keyName, valueName, key = '', value = '') {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'header-entry'; div.className = 'header-entry';
@ -107,22 +70,58 @@ export function addKeyValueInput(container, keyName, valueName, key = '', value
container.appendChild(div); 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 = `
<input type="text" name="${inputName}" placeholder="${placeholder}" value="${value}">
<button type="button" class="btn-icon" onclick="this.parentElement.remove()" title="移除此上游">
<i class="fa-solid fa-xmark"></i>
</button>`;
container.appendChild(div);
}
export function fillForm(config, originalFilename) { export function fillForm(config, originalFilename) {
DOMElements.originalFilenameInput.value = originalFilename; DOMElements.originalFilenameInput.value = originalFilename;
DOMElements.domainInput.value = config.domain; DOMElements.domainInput.value = config.domain_config?.domain || '';
document.getElementById('upstream').value = config.upstream?.upstream || '';
document.getElementById('file_dir_path').value = config.file_server?.file_dir_path || ''; const enableUpstream = config.upstream_config?.enable_upstream || false;
document.getElementById('enable_browser').checked = config.file_server?.enable_browser || false; const enableFileServer = config.file_server_config?.enable_file_server || false;
const selectContainer = document.getElementById('custom-select-tmpl'); let mode = 'none';
selectContainer.querySelector('.select-selected').textContent = config.tmpl_type; if (enableUpstream) mode = 'reverse_proxy';
selectContainer.querySelector('input[type="hidden"]').value = config.tmpl_type; 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 = ''; 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))); if (config.headers) Object.entries(config.headers).forEach(([k, v]) => v.forEach(val => addKeyValueInput(DOMElements.headersContainer, 'header_key', 'header_value', k, val)));
DOMElements.upstreamHeadersContainer.innerHTML = '';
if (config.upstream?.upstream_headers) Object.entries(config.upstream.upstream_headers).forEach(([k, v]) => v.forEach(val => addKeyValueInput(DOMElements.upstreamHeadersContainer, 'upstream_header_key', 'upstream_header_value', k, val))); document.getElementById('enable_log').checked = config.log_config?.enable_log || false;
document.getElementById('enable_log').checked = config.log?.enable_log || false; document.getElementById('enable_error_page').checked = config.error_page_config?.enable_error_page || false;
document.getElementById('enable_error_page').checked = config.error_page?.enable_error_page || false; document.getElementById('enable_encode').checked = config.encode_config?.enable_encode || false;
document.getElementById('enable_encode').checked = config.encode?.enable_encode || false;
updateMultiUpstreamView(DOMElements.mutiUpstreamCheckbox.checked);
} }
export function showRenderedConfig(configs, filename) { export function showRenderedConfig(configs, filename) {
@ -168,9 +167,27 @@ export function updateCaddyStatusView(status, handlers) {
dot.classList.add(dotClass); dot.classList.add(dotClass);
} }
export function updateFormVisibility(selectedTemplate, availableTemplates) { export function updateServiceModeView(mode) {
const showUpstream = selectedTemplate === 'reverse_proxy' && availableTemplates.includes('reverse_proxy'); DOMElements.upstreamFieldset.classList.toggle('hidden', mode !== 'reverse_proxy');
const showFileServer = selectedTemplate === 'file_server' && availableTemplates.includes('file_server'); DOMElements.fileserverFieldset.classList.toggle('hidden', mode !== 'file_server');
DOMElements.upstreamFieldset.classList.toggle('hidden', !showUpstream); }
DOMElements.fileserverFieldset.classList.toggle('hidden', !showFileServer);
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');
const controlRect = control.getBoundingClientRect();
const buttonRect = activeButton.getBoundingClientRect();
slider.style.width = `${buttonRect.width}px`;
slider.style.transform = `translateX(${activeButton.offsetLeft - control.clientLeft - 4}px)`;
} }