add multupstream && new select
This commit is contained in:
parent
b10790c212
commit
30da719d57
4 changed files with 218 additions and 163 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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">
|
</fieldset>
|
||||||
<label>模板类型</label>
|
|
||||||
<div id="custom-select-tmpl" class="custom-select"></div>
|
<fieldset>
|
||||||
</div>
|
<legend>服务模式</legend>
|
||||||
</div>
|
<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">
|
<fieldset id="upstream-fieldset" class="hidden">
|
||||||
<legend>反向代理 (Upstream)</legend>
|
<legend>反向代理配置</legend>
|
||||||
<div class="form-group">
|
|
||||||
|
<!-- 单上游输入框, 在多上游模式下隐藏 -->
|
||||||
|
<div class="form-group" id="single-upstream-group">
|
||||||
<label for="upstream">上游服务地址</label>
|
<label for="upstream">上游服务地址</label>
|
||||||
<input type="text" id="upstream" name="upstream" placeholder="例如: 127.0.0.1:8080">
|
<input type="text" id="upstream" name="upstream" placeholder="例如: 127.0.0.1:8080">
|
||||||
</div>
|
</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">
|
<div class="sub-fieldset">
|
||||||
<p class="sub-legend">上游请求头 (Upstream Headers)</p>
|
<p class="sub-legend">上游请求头 (Upstream Headers)</p>
|
||||||
<div id="upstream-headers-container"></div>
|
<div id="upstream-headers-container"></div>
|
||||||
<button type="button" id="add-upstream-header-btn" class="btn btn-secondary btn-small">
|
<button type="button" id="add-upstream-header-btn" class="btn btn-secondary btn-small"><i class="fa-solid fa-plus"></i> 添加上游请求头</button>
|
||||||
<i class="fa-solid fa-plus"></i> 添加上游请求头
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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)`;
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue