add i18n step1

This commit is contained in:
wjqserver 2025-07-01 07:04:39 +08:00
parent 34d553a890
commit 79e3db6078
23 changed files with 2309 additions and 450 deletions

View file

@ -2,13 +2,12 @@
import { state } from './state.js';
import { api } from './api.js';
import { theme, activateNav } from './common.js';
import { initializePage } from './common.js';
import { t } from './locale.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;
// --- 事件处理与逻辑流 (所有 handle* 函数保持不变) ---
function getFormStateAsString() {
const formData = new FormData(DOMElements.configForm);
@ -27,14 +26,18 @@ function getFormStateAsString() {
}
async function attemptExitForm() {
if (getFormStateAsString() !== state.initialFormState) {
if (await notification.confirm('您有未保存的更改。确定要放弃吗?')) switchView(DOMElements.configListPanel);
} else switchView(DOMElements.configListPanel);
if (await getFormStateAsString() !== state.initialFormState) {
if (await notification.confirm(t('dialogs.unsaved_changes_msg'), t('dialogs.unsaved_changes_title'))) {
switchView(DOMElements.configListPanel);
}
} else {
switchView(DOMElements.configListPanel);
}
}
async function handleLogout() {
if (!await notification.confirm('您确定要退出登录吗?')) return;
notification.toast('正在退出...', 'info');
if (!await notification.confirm(t('dialogs.logout_msg'))) return;
notification.toast(t('toasts.logout_processing'), 'info');
setTimeout(() => { window.location.href = `/v0/api/auth/logout`; }, 500);
}
@ -42,7 +45,7 @@ async function loadAllConfigs() {
try {
const filenames = await api.get('/config/filenames');
renderConfigList(filenames);
} catch (error) { if (error.message) notification.toast(`加载配置列表失败: ${error.message}`, 'error'); }
} catch (error) { if (error.message) notification.toast(t('toasts.load_configs_error', { error: error.message }), 'error'); }
}
async function handleEditConfig(originalFilename) {
@ -53,22 +56,22 @@ async function handleEditConfig(originalFilename) {
]);
state.isEditing = true;
switchView(DOMElements.configFormPanel);
DOMElements.formTitle.textContent = '编辑配置';
DOMElements.formTitle.textContent = t('pages.configs.form_title_edit');
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'); }
state.initialFormState = await getFormStateAsString();
} catch (error) { notification.toast(t('toasts.load_config_detail_error', { error: error.message }), 'error'); }
}
async function handleDeleteConfig(filename) {
if (!await notification.confirm(`确定要删除配置 "${filename}" 吗?`)) return;
if (!await notification.confirm(t('dialogs.delete_config_msg', { filename }), t('dialogs.delete_config_title'))) return;
try {
await api.delete(`/config/file/${filename}`);
notification.toast('配置已成功删除。', 'success');
notification.toast(t('toasts.delete_success'), 'success');
loadAllConfigs();
} catch (error) { notification.toast(`删除失败: ${error.message}`, 'error'); }
} catch (error) { notification.toast(t('toasts.delete_error', { error: error.message }), 'error'); }
}
async function handleSaveConfig(e) {
@ -76,7 +79,7 @@ async function handleSaveConfig(e) {
const formData = new FormData(DOMElements.configForm);
const domain = formData.get('domain');
if (!domain) {
notification.toast('域名不能为空。', 'error');
notification.toast(t('toasts.error_domain_empty'), 'error');
return;
}
const getHeadersMap = (keyName, valueName) => {
@ -116,23 +119,23 @@ async function handleSaveConfig(e) {
try {
const result = await api.put(`/config/file/${domain}`, configData);
state.isEditing = false;
notification.toast(result.message || '配置已成功保存。', 'success');
notification.toast(result.message || t('toasts.save_success'), 'success');
setTimeout(() => {
switchView(DOMElements.configListPanel);
loadAllConfigs();
}, 500);
} catch (error) { notification.toast(`保存失败: ${error.message}`, 'error'); }
} catch (error) { notification.toast(t('toasts.save_error', { error: 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');
notification.toast(t('toasts.no_presets_available'), 'info');
return;
}
const presetId = await createPresetSelectionModal(applicablePresets);
if (!presetId) return; // 用户取消了选择
const presetId = await createPresetSelectionModal(applicablePresets, t);
if (!presetId) return;
let targetContainer, keyName, valueName;
if (targetType === 'global') {
@ -147,18 +150,17 @@ async function openPresetModal(targetType) {
return;
}
// 在获取到presetId后再执行填充逻辑
try {
notification.toast('正在加载预设...', 'info', 1000);
notification.toast(t('toasts.loading_preset'), 'info', 1000);
const preset = await api.get(`/config/headers-presets/${presetId}`);
if (!preset.headers) {
notification.toast('此预设无数据。', 'info');
notification.toast(t('toasts.preset_no_data'), 'info');
return;
}
const choice = await notification.confirm('如何填充预设?', '选择填充方式', { confirmText: '追加', cancelText: '替换' });
const choice = await notification.confirm(t('dialogs.preset_fill_msg'), t('dialogs.preset_fill_title'), { confirmText: t('common.append'), cancelText: t('common.replace') });
if (choice === false) { // 用户选择了“替换”
if (choice === false) {
targetContainer.innerHTML = '';
}
@ -167,19 +169,16 @@ async function openPresetModal(targetType) {
addKeyValueInput(targetContainer, keyName, valueName, key, value);
});
});
notification.toast(`已成功填充预设 "${preset.name}"`, 'success');
notification.toast(t('toasts.preset_fill_success', { presetName: preset.name }), 'success');
} catch (error) {
notification.toast(`加载预设失败: ${error.message}`, 'error');
notification.toast(t('toasts.load_preset_error', { error: error.message }), 'error');
}
}
function init() {
theme.init(DOMElements.themeToggleInput);
notification.init(DOMElements.toastContainer, DOMElements.dialogContainer, DOMElements.modalContainer);
activateNav('configs');
initCaddyStatus();
// --- 初始化与事件绑定 ---
function pageInit() {
// 这个函数包含所有特定于 app.js 的初始化逻辑
loadAllConfigs();
api.get('/config/headers-presets')
@ -187,16 +186,13 @@ function init() {
state.headerPresets = presets || [];
})
.catch(err => {
if (err.message) notification.toast(`加载Header预设失败: ${err.message}`, 'error');
if (err.message) notification.toast(t('toasts.load_presets_error', { error: 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', () => {
DOMElements.addNewConfigBtn.addEventListener('click', async () => {
state.isEditing = false;
switchView(DOMElements.configFormPanel);
DOMElements.formTitle.textContent = '创建新配置';
DOMElements.formTitle.textContent = t('pages.configs.form_title_create');
DOMElements.configForm.reset();
const noneButton = DOMElements.serviceModeControl.querySelector('[data-mode="none"]');
@ -204,7 +200,7 @@ function init() {
updateServiceModeView('none');
updateMultiUpstreamView(false);
state.initialFormState = getFormStateAsString();
state.initialFormState = await getFormStateAsString();
DOMElements.headersContainer.innerHTML = '';
DOMElements.upstreamHeadersContainer.innerHTML = '';
DOMElements.multiUpstreamContainer.innerHTML = '';
@ -247,7 +243,7 @@ function init() {
DOMElements.addMultiUpstreamBtn.addEventListener('click', (e) => {
e.preventDefault();
addSingleInput(DOMElements.multiUpstreamContainer, 'upstream_servers', '例如: 127.0.0.1:8081');
addSingleInput(DOMElements.multiUpstreamContainer, 'upstream_servers', t('form.upstream_server_placeholder'));
});
DOMElements.serviceModeControl.addEventListener('click', (e) => {
@ -265,4 +261,5 @@ function init() {
});
}
init();
// 使用通用初始化函数启动页面
initializePage({ pageId: 'configs', pageInit: pageInit });

View file

@ -6,6 +6,9 @@ import { notification } from './notifications.js';
let caddyStatusInterval;
const POLLING_INTERVAL = 5000;
// 将 t 函数保存在模块作用域内
let translate;
const DOMElements = {
caddyStatusIndicator: document.getElementById('caddy-status-indicator'),
caddyActionButtonContainer: document.getElementById('caddy-action-button-container'),
@ -24,23 +27,31 @@ function updateCaddyStatusView(status) {
const text = DOMElements.caddyStatusIndicator.querySelector('.status-text');
const buttonContainer = DOMElements.caddyActionButtonContainer;
if(!dot || !text || !buttonContainer) return; // 如果元素不存在,则不执行
if(!dot || !text || !buttonContainer) return;
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));
statusText = translate('status.running');
dotClass = 'running';
buttonContainer.appendChild(createButton(translate('caddy.reload_btn'), 'btn-warning', handleReloadCaddy));
buttonContainer.appendChild(createButton(translate('caddy.stop_btn'), 'btn-danger', handleStopCaddy));
break;
case 'stopped':
statusText = '已停止'; dotClass = 'stopped';
buttonContainer.appendChild(createButton('启动 Caddy', 'btn-success', handleStartCaddy));
statusText = translate('status.stopped');
dotClass = 'stopped';
buttonContainer.appendChild(createButton(translate('caddy.start_btn'), 'btn-success', handleStartCaddy));
break;
case 'checking':
statusText = translate('status.checking');
dotClass = 'checking';
break;
default:
statusText = translate('status.unknown');
dotClass = 'error';
break;
case 'checking': statusText = '检查中...'; dotClass = 'checking'; break;
default: statusText = '状态未知'; dotClass = 'error'; break;
}
text.textContent = statusText;
dot.classList.add(dotClass);
@ -59,35 +70,38 @@ async function checkCaddyStatus() {
async function handleStartCaddy() {
try {
const result = await api.post('/caddy/run');
notification.toast(result.message || '启动命令已发送。', 'success');
notification.toast(result.message || translate('toasts.start_cmd_sent'), 'success');
setTimeout(checkCaddyStatus, 500);
} catch (error) { notification.toast(`启动失败: ${error.message}`, 'error'); }
} catch (error) { notification.toast(translate('toasts.start_error', { error: error.message }), 'error'); }
}
async function handleStopCaddy() {
if (!await notification.confirm('您确定要停止 Caddy 实例吗?')) return;
if (!await notification.confirm(translate('dialogs.stop_caddy_msg'))) return;
try {
const result = await api.post('/caddy/stop');
notification.toast(result.message || '停止命令已发送。', 'info');
notification.toast(result.message || translate('toasts.stop_cmd_sent'), 'info');
setTimeout(checkCaddyStatus, 500);
} catch(error) { notification.toast(`操作失败: ${error.message}`, 'error'); }
} catch(error) { notification.toast(translate('toasts.action_error', { error: error.message }), 'error'); }
}
async function handleReloadCaddy() {
if (!await notification.confirm('确定要重载 Caddy 配置吗?')) return;
if (!await notification.confirm(translate('dialogs.reload_caddy_msg'))) return;
try {
const result = await api.post('/caddy/restart');
notification.toast(result.message || '重载命令已发送。', 'success');
notification.toast(result.message || translate('toasts.reload_sent'), 'success');
setTimeout(checkCaddyStatus, 500);
} catch(error) { notification.toast(`重载失败: ${error.message}`, 'error'); }
} catch(error) { notification.toast(translate('toasts.reload_error', { error: error.message }), 'error'); }
}
export function initCaddyStatus() {
// 确保通知模块已经初始化
// initCaddyStatus 现在接收 t 函数作为参数
export function initCaddyStatus(translator) {
// 保存翻译函数以供模块内其他函数使用
translate = translator;
const dialogContainer = document.getElementById('dialog-container');
const toastContainer = document.getElementById('toast-container');
if (dialogContainer && toastContainer) {
notification.init(toastContainer, dialogContainer);
notification.init(toastContainer, dialogContainer, null, translate); // 将 t 函数传递给通知模块
}
checkCaddyStatus();

View file

@ -1,5 +1,10 @@
// js/common.js - 存放共享模块
import { initI18n, t } from './locale.js';
import { notification } from './notifications.js';
import { initCaddyStatus } from './caddy.js';
import { initUI } from './ui.js';
const theme = {
init: (toggleElement) => {
const storedTheme = localStorage.getItem('theme');
@ -55,5 +60,71 @@ function activateNav(pageId) {
});
}
/**
* 初始化移动端侧边栏的开关逻辑
*/
function initSidebar() {
const sidebar = document.getElementById('sidebar');
const menuToggleBtn = document.getElementById('menu-toggle-btn');
// 动态创建并管理遮罩层
let overlay = document.querySelector('.sidebar-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
document.body.appendChild(overlay);
}
const openSidebar = () => {
if (!sidebar || !overlay) return;
sidebar.classList.add('is-open');
overlay.classList.add('is-visible');
};
const closeSidebar = () => {
if (!sidebar || !overlay) return;
sidebar.classList.remove('is-open');
overlay.classList.remove('is-visible');
};
if (menuToggleBtn) {
menuToggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
openSidebar();
});
}
overlay.addEventListener('click', closeSidebar);
}
/**
* 通用的页面初始化函数
* @param {object} options - 初始化选项
* @param {string} options.pageId - 当前页面的ID, 用于导航高亮
* @param {function} [options.pageInit=null] - 特定于该页面的额外初始化逻辑
*/
export async function initializePage(options) {
// 1. 初始化国际化 (最高优先级)
await initI18n();
initUI(t);
// 2. 初始化UI模块和通用功能
theme.init(document.getElementById('theme-toggle-input'));
notification.init(
document.getElementById('toast-container'),
document.getElementById('dialog-container'),
document.getElementById('modal-container')
);
activateNav(options.pageId);
initSidebar();
initCaddyStatus(t);
// 3. 如果有特定页面的初始化逻辑, 则执行它
if (options.pageInit && typeof options.pageInit === 'function') {
options.pageInit();
}
}
// 导出模块
export { theme, toast, activateNav };

View file

@ -1,25 +1,21 @@
// js/global.js - 全局配置页面的逻辑
import { theme, activateNav } from './common.js';
import { initializePage } from './common.js'; // 导入通用初始化函数
import { api } from './api.js';
import { notification } from './notifications.js';
import { initCaddyStatus } from './caddy.js';
import { createCustomSelect } from './ui.js';
const DOMElements = {
globalForm: document.getElementById('global-caddy-form'),
themeToggleInput: document.getElementById('theme-toggle-input'),
logoutBtn: document.getElementById('logout-btn'),
toastContainer: document.getElementById('toast-container'),
dialogContainer: document.getElementById('dialog-container'),
enableDnsChallengeCheckbox: document.getElementById('enable_dns_challenge'),
globalTlsConfigGroup: document.getElementById('global-tls-config-group'),
enableEchCheckbox: document.getElementById('enable_ech'),
echConfigGroup: document.getElementById('ech-config-group'),
};
const submitButton = DOMElements.globalForm.querySelector('button[type="submit"]');
// submitButton 在 pageInit 中获取, 确保DOM已加载
let submitButton;
// 从表单收集数据, 构建成后端需要的JSON结构
function getGlobalConfigFromForm() {
const formData = new FormData(DOMElements.globalForm);
const enableEch = DOMElements.enableEchCheckbox.checked;
@ -49,7 +45,6 @@ function getGlobalConfigFromForm() {
};
}
// 用从API获取的数据填充表单
function fillGlobalConfigForm(config) {
if (!config) return;
@ -100,7 +95,6 @@ async function handleSaveGlobalConfig(e) {
submitButton.querySelector('span').textContent = "保存中...";
try {
// 修正: 更新API端点路径
const result = await api.put('/global/config', configData);
notification.toast(result.message || '全局配置已成功保存Caddy正在重载...', 'success');
} catch (error) {
@ -118,23 +112,19 @@ async function handleLogout() {
}
}
function init() {
theme.init(DOMElements.themeToggleInput);
notification.init(DOMElements.toastContainer, DOMElements.dialogContainer);
activateNav('global');
initCaddyStatus();
// 页面特有的初始化逻辑
function pageInit() {
// 在这里获取 submitButton, 确保 DOM 已加载
submitButton = DOMElements.globalForm.querySelector('button[type="submit"]');
// 修正: 更新API端点路径
api.get('/global/log/levels')
.then(levels => createCustomSelect('select-log-level', Object.keys(levels)))
.catch(err => notification.toast(`加载日志级别失败: ${err.message}`, 'error'));
// 修正: 更新API端点路径
api.get('/global/tls/providers')
.then(providers => createCustomSelect('select-tls-provider', Object.keys(providers)))
.catch(err => notification.toast(`加载TLS提供商失败: ${err.message}`, 'error'));
// 修正: 更新API端点路径
api.get('/global/config')
.then(config => fillGlobalConfigForm(config))
.catch(err => notification.toast(`加载全局配置失败: ${err.message}`, 'error'));
@ -150,4 +140,5 @@ function init() {
});
}
init();
// 使用通用初始化函数启动页面
initializePage({ pageId: 'global', pageInit: pageInit });

View file

@ -1,150 +1,215 @@
// js/init.js
// js/init.js - 初始化页面的独立逻辑
document.addEventListener('DOMContentLoaded', () => {
const DOMElements = {
initForm: document.getElementById('init-form'),
initButton: null,
toastContainer: document.getElementById('toast-container'),
usernameInput: document.getElementById('username'),
passwordInput: document.getElementById('password'),
confirmPasswordInput: document.getElementById('confirm_password'),
langSwitcherBtn: document.getElementById('lang-switcher-btn'),
langOptionsList: document.getElementById('lang-options-list'), // 从第一个片段引入
};
const initButton = DOMElements.initForm.querySelector('button[type="submit"]');
const INIT_API_URL = '/v0/api/auth/init';
const PASSWORD_MIN_LENGTH = 8;
const TOAST_DEFAULT_DURATION = 3000;
const REDIRECT_DELAY_SUCCESS = 1500;
const theme = {
init: () => {
const storedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const currentTheme = storedTheme || (systemPrefersDark ? 'dark' : 'light');
document.documentElement.dataset.theme = currentTheme;
}
};
const toast = {
_container: null,
_icons: { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle' },
init: (containerElement) => {
if (!containerElement) {
console.error('Toast container element not found.');
return;
const i18n = {
currentLocale: {},
currentLang: 'en',
// 从第一个片段引入, 使用对象更方便显示语言名称
supportedLangs: { 'en': 'English', 'zh-CN': '简体中文' },
t: function(key, replacements = {}) {
const translation = key.split('.').reduce((obj, k) => obj && obj[k], this.currentLocale) || key;
let result = translation;
if (typeof result === 'string') {
for (const placeholder in replacements) {
result = result.replace(`{${placeholder}}`, replacements[placeholder]);
}
}
toast._container = containerElement;
toast._container.addEventListener('click', (e) => {
if (e.target.dataset.toastClose !== undefined) {
toast._hideToast(e.target.closest('.toast'));
return result;
},
applyTranslations: function() {
// 优化后的翻译应用逻辑, 优先更新span, 其次更新非空文本节点, 最后直接更新元素文本
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.dataset.i18n;
const translation = this.t(key);
if (translation !== key) { // 仅当找到翻译时才应用
const spanChild = el.querySelector('span');
if (spanChild) {
spanChild.textContent = translation;
} else {
// 查找直接的、非空文本节点进行替换
const textNode = Array.from(el.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0);
if (textNode) {
textNode.textContent = translation;
} else {
// 备用方案: 直接设置元素的textContent
el.textContent = translation;
}
}
}
});
// 从第一个片段引入, 处理data-i18n-title属性
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.title = this.t(el.dataset.i18nTitle);
});
document.title = this.t('pages.init.page_title');
},
show: (message, type = 'info', duration = TOAST_DEFAULT_DURATION) => {
if (!toast._container) {
console.error('Toast module not initialized. Container is missing.');
return;
loadLocale: async function(lang) {
try {
const response = await fetch(`/locales/${lang}.json`);
if (!response.ok) throw new Error('File not found');
this.currentLocale = await response.json();
this.currentLang = lang;
document.documentElement.lang = lang; // 设置HTML语言属性
localStorage.setItem('appLanguage', lang); // 从第一个片段引入, 保存到localStorage
} catch (e) {
console.error(`Could not load locale for ${lang}, using fallback.`, e);
this.currentLocale = {};
}
},
init: async function() {
// 从第一个片段引入, 优先使用保存的语言, 其次使用浏览器语言
const savedLang = localStorage.getItem('appLanguage');
const browserLang = navigator.language.startsWith('zh') ? 'zh-CN' : 'en';
const langToLoad = savedLang || browserLang;
await this.loadLocale(langToLoad);
this.applyTranslations();
this.populateLangOptions(); // 从第一个片段引入, 初始化语言选项列表
},
// 从第一个片段引入, 用于动态生成语言选项列表
populateLangOptions: function() {
// 清空现有选项
DOMElements.langOptionsList.innerHTML = '';
for (const [code, name] of Object.entries(this.supportedLangs)) {
const li = document.createElement('li');
li.dataset.lang = code;
li.textContent = name;
if (code === this.currentLang) {
li.classList.add('active'); // 标记当前选中语言
}
DOMElements.langOptionsList.appendChild(li);
}
}
// 移除 i18n.toggleLanguage, 因为有新的语言选择机制
};
const iconClass = toast._icons[type] || 'fa-info-circle';
// 从第二个片段完整引入toast对象
const toast = {
show: function(message, type = 'info', duration = 3000) {
if (!DOMElements.toastContainer) return;
const icons = { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle' };
const toastElement = document.createElement('div');
toastElement.className = `toast ${type}`;
toastElement.innerHTML = `
<i class="toast-icon fa-solid ${iconClass}"></i>
<p class="toast-message">${message}</p>
<button class="toast-close" data-toast-close>×</button>
`;
toast._container.appendChild(toastElement);
toastElement.innerHTML = `<i class="toast-icon fa-solid ${icons[type]}"></i><p class="toast-message">${message}</p><button class="toast-close" data-toast-close>×</button>`;
DOMElements.toastContainer.appendChild(toastElement);
requestAnimationFrame(() => toastElement.classList.add('show'));
setTimeout(() => toast._hideToast(toastElement), duration);
const timeoutId = setTimeout(() => this._hide(toastElement), duration);
toastElement.querySelector('[data-toast-close]').addEventListener('click', () => {
clearTimeout(timeoutId);
this._hide(toastElement);
});
},
_hideToast: (toastElement) => {
_hide: function(toastElement) {
if (!toastElement) return;
toastElement.classList.remove('show');
toastElement.addEventListener('transitionend', () => toastElement.remove(), { once: true });
}
};
// 从第二个片段完整引入handleInitSubmit函数
async function handleInitSubmit(e) {
e.preventDefault();
const username = DOMElements.initForm.username.value.trim();
const password = DOMElements.initForm.password.value.trim();
const confirmPassword = DOMElements.initForm.confirm_password.value.trim();
// 1. 获取并修剪输入值
const username = DOMElements.usernameInput.value.trim(); // 获取用户名并去除前后空格
const password = DOMElements.passwordInput.value.trim();
const confirmPassword = DOMElements.confirmPasswordInput.value.trim();
// 2. 添加用户名输入框的空值验证
if (username === '') {
toast.show('管理员用户名不能为空', 'error');
DOMElements.usernameInput.focus();
toast.show(i18n.t('toasts.error_username_empty'), 'error');
DOMElements.initForm.username.focus();
return;
}
// 其他密码验证逻辑不变
if (password === '') {
toast.show('密码不能为空', 'error');
DOMElements.passwordInput.focus();
toast.show(i18n.t('toasts.error_password_empty'), 'error');
DOMElements.initForm.password.focus();
return;
}
if (confirmPassword === '') {
toast.show('确认密码不能为空', 'error');
DOMElements.confirmPasswordInput.focus();
return;
}
if (password !== confirmPassword) {
toast.show('两次输入的密码不匹配', 'error');
DOMElements.passwordInput.focus();
toast.show(i18n.t('toasts.init_error_mismatch'), 'error');
DOMElements.initForm.confirm_password.focus();
return;
}
if (password.length < PASSWORD_MIN_LENGTH) {
toast.show(`密码长度至少为 ${PASSWORD_MIN_LENGTH}`, 'error');
DOMElements.passwordInput.focus();
toast.show(i18n.t('toasts.init_error_short', { minLength: PASSWORD_MIN_LENGTH }), 'error');
DOMElements.initForm.password.focus();
return;
}
DOMElements.initButton.disabled = true;
DOMElements.initButton.querySelector('span').textContent = '设置中...';
initButton.disabled = true;
initButton.querySelector('span').textContent = i18n.t('pages.init.setting_up_btn');
try {
const formData = new FormData(DOMElements.initForm);
formData.set('username', username); // 确保发送的是修剪过的用户名
formData.set('password', password); // 确保发送的是修剪过的密码
formData.delete('confirm_password');
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
const response = await fetch(INIT_API_URL, {
method: 'POST',
body: new URLSearchParams(formData)
});
const result = await response.json();
const result = await response.json();
if (response.ok) {
toast.show('管理员账户创建成功!正在跳转到登录页面...', 'success');
setTimeout(() => { window.location.href = '/login.html'; }, REDIRECT_DELAY_SUCCESS);
toast.show(i18n.t('toasts.init_success'), 'success');
setTimeout(() => { window.location.href = '/login.html'; }, 1500);
} else {
throw new Error(result.error || `初始化失败: ${response.status}`);
throw new Error(result.error || i18n.t('toasts.init_error_generic'));
}
} catch (error) {
toast.show(error.message, 'error');
DOMElements.initButton.disabled = false;
DOMElements.initButton.querySelector('span').textContent = '完成设置';
initButton.disabled = false;
initButton.querySelector('span').textContent = i18n.t('pages.init.setup_btn');
}
}
function initApp() {
theme.init();
async function initApp() {
// 主题设置逻辑
const storedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.dataset.theme = storedTheme || (systemPrefersDark ? 'dark' : 'light');
// 初始化国际化
await i18n.init();
// 初始化表单事件监听
if (DOMElements.initForm) {
DOMElements.initButton = DOMElements.initForm.querySelector('button[type="submit"]');
toast.init(DOMElements.toastContainer);
DOMElements.initForm.addEventListener('submit', handleInitSubmit);
} else {
console.error('Init form element not found. Script may not function correctly.');
}
// 语言切换按钮事件监听 (从第一个片段引入)
if (DOMElements.langSwitcherBtn) {
DOMElements.langSwitcherBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡, 防止立即触发document的点击事件
DOMElements.langOptionsList.classList.toggle('hidden');
});
}
// 语言选项列表事件监听 (从第一个片段引入)
if (DOMElements.langOptionsList) {
DOMElements.langOptionsList.addEventListener('click', async (e) => {
const target = e.target.closest('li[data-lang]'); // 查找最近的语言li元素
if (target) {
await i18n.loadLocale(target.dataset.lang); // 加载新语言
i18n.applyTranslations(); // 应用翻译
i18n.populateLangOptions(); // 更新语言选项列表的激活状态
DOMElements.langOptionsList.classList.add('hidden'); // 隐藏列表
}
});
}
// 文档点击事件, 用于点击外部时隐藏语言选项列表 (从第一个片段引入)
document.addEventListener('click', () => {
if (DOMElements.langOptionsList && !DOMElements.langOptionsList.classList.contains('hidden')) {
DOMElements.langOptionsList.classList.add('hidden');
}
});
}
initApp();
});

View file

@ -10,7 +10,7 @@ const supportedLangs = ['en', 'zh-CN']; // 应用支持的语言列表
*/
async function loadLocale(lang) {
try {
const response = await fetch(`/locales/${lang}.json?v=${Date.now()}`);
const response = await fetch(`/locales/${lang}.json`);
if (!response.ok) {
throw new Error(`Language file for ${lang} not found (status: ${response.status}).`);
}
@ -49,7 +49,14 @@ function applyTranslationsToDOM() {
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.dataset.i18nTitle;
const translation = t(key);
if(translation !== key) el.title = translation;
if (translation !== key) el.title = translation;
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.dataset.i18nPlaceholder;
const translation = t(key);
if (translation !== key && el.placeholder !== undefined) { // 确保元素有placeholder属性
el.placeholder = translation;
}
});
}
@ -62,7 +69,7 @@ function applyTranslationsToDOM() {
export function t(key, replacements = {}) {
// 通过路径 'a.b.c' 在嵌套对象中查找值: currentLocale['a']['b']['c']
const translation = key.split('.').reduce((obj, k) => obj && obj[k], currentLocale);
let result = translation || key; // 如果找不到, 返回原始key作为回退
// 处理占位符替换, e.g., {filename: 'example.com'}
@ -71,7 +78,7 @@ export function t(key, replacements = {}) {
result = result.replace(`{${placeholder}}`, replacements[placeholder]);
}
}
return result;
}
@ -106,4 +113,8 @@ export async function setLanguage(lang) {
localStorage.setItem('appLanguage', lang);
window.location.reload(); // 刷新页面以应用所有翻译是最简单可靠的方式
}
}
export function getCurrentLanguage() {
return currentLang;
}

View file

@ -1,64 +1,139 @@
// js/login.js - 登录页面的独立逻辑
document.addEventListener('DOMContentLoaded', () => {
const DOMElements = {
loginForm: document.getElementById('login-form'),
toastContainer: document.getElementById('toast-container'),
langSwitcherBtn: document.getElementById('lang-switcher-btn'),
langOptionsList: document.getElementById('lang-options-list'), // 从第一个片段引入
};
const loginButton = DOMElements.loginForm.querySelector('button[type="submit"]');
const LOGIN_API_URL = '/v0/api/auth/login';
const theme = {
init: () => {
const storedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const currentTheme = storedTheme || (systemPrefersDark ? 'dark' : 'light');
document.documentElement.dataset.theme = currentTheme;
const i18n = {
currentLocale: {},
currentLang: 'en',
// 从第一个片段引入, 使用对象更方便显示语言名称
supportedLangs: { 'en': 'English', 'zh-CN': '简体中文' },
t: function(key, replacements = {}) {
const translation = key.split('.').reduce((obj, k) => obj && obj[k], this.currentLocale) || key;
let result = translation;
if (typeof result === 'string') {
for (const placeholder in replacements) {
result = result.replace(`{${placeholder}}`, replacements[placeholder]);
}
}
return result;
},
applyTranslations: function() {
// 优化后的翻译应用逻辑, 优先更新span, 其次更新非空文本节点, 最后直接更新元素文本
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.dataset.i18n;
const translation = this.t(key);
if (translation !== key) { // 仅当找到翻译时才应用
const spanChild = el.querySelector('span');
if (spanChild) {
spanChild.textContent = translation;
} else {
// 查找直接的、非空文本节点进行替换
const textNode = Array.from(el.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0);
if (textNode) {
textNode.textContent = translation;
} else {
// 备用方案: 直接设置元素的textContent
el.textContent = translation;
}
}
}
});
// 从第一个片段引入, 处理data-i18n-title属性
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.title = this.t(el.dataset.i18nTitle);
});
document.title = this.t('pages.login.page_title');
},
loadLocale: async function(lang) {
try {
const response = await fetch(`/locales/${lang}.json`);
if (!response.ok) throw new Error('File not found');
this.currentLocale = await response.json();
this.currentLang = lang;
document.documentElement.lang = lang; // 设置HTML语言属性
localStorage.setItem('appLanguage', lang); // 从第一个片段引入, 保存到localStorage
} catch (e) {
console.error(`Could not load locale for ${lang}, using fallback.`, e);
this.currentLocale = {};
}
},
init: async function() {
// 从第一个片段引入, 优先使用保存的语言, 其次使用浏览器语言
const savedLang = localStorage.getItem('appLanguage');
const browserLang = navigator.language.startsWith('zh') ? 'zh-CN' : 'en';
const langToLoad = savedLang || browserLang;
await this.loadLocale(langToLoad);
this.applyTranslations();
this.populateLangOptions(); // 从第一个片段引入, 初始化语言选项列表
},
// 从第一个片段引入, 用于动态生成语言选项列表
populateLangOptions: function() {
// 清空现有选项
DOMElements.langOptionsList.innerHTML = '';
for (const [code, name] of Object.entries(this.supportedLangs)) {
const li = document.createElement('li');
li.dataset.lang = code;
li.textContent = name;
if (code === this.currentLang) {
li.classList.add('active'); // 标记当前选中语言
}
DOMElements.langOptionsList.appendChild(li);
}
}
// 移除 i18n.toggleLanguage, 因为有新的语言选择机制
};
// 从第二个片段完整引入toast对象
const toast = {
show: (message, type = 'info', duration = 3000) => {
show: function(message, type = 'info', duration = 3000) {
if (!DOMElements.toastContainer) return;
const icons = { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle' };
const iconClass = icons[type] || 'fa-info-circle';
const toastElement = document.createElement('div');
toastElement.className = `toast ${type}`;
toastElement.innerHTML = `<i class="toast-icon fa-solid ${iconClass}"></i><p class="toast-message">${message}</p><button class="toast-close" data-toast-close>×</button>`;
toastElement.innerHTML = `<i class="toast-icon fa-solid ${icons[type]}"></i><p class="toast-message">${message}</p><button class="toast-close" data-toast-close>×</button>`;
DOMElements.toastContainer.appendChild(toastElement);
requestAnimationFrame(() => toastElement.classList.add('show'));
const timeoutId = setTimeout(() => hideToast(toastElement), duration);
const timeoutId = setTimeout(() => this._hide(toastElement), duration);
toastElement.querySelector('[data-toast-close]').addEventListener('click', () => {
clearTimeout(timeoutId);
hideToast(toastElement);
this._hide(toastElement);
});
},
_hide: function(toastElement) {
if (!toastElement) return;
toastElement.classList.remove('show');
toastElement.addEventListener('transitionend', () => toastElement.remove(), { once: true });
}
};
function hideToast(toastElement) {
if (!toastElement) return;
toastElement.classList.remove('show');
toastElement.addEventListener('transitionend', () => toastElement.remove(), { once: true });
}
// 从第二个片段完整引入handleLogin函数
async function handleLogin(e) {
e.preventDefault();
// 获取并确认值
const username = DOMElements.loginForm.username.value.trim();
const password = DOMElements.loginForm.password.value.trim();
if (username === '') {
toast.show('用户名不能为空', 'error');
toast.show(i18n.t('toasts.error_username_empty'), 'error');
DOMElements.loginForm.username.focus();
return;
}
if (password === '') {
toast.show('密码不能为空', 'error');
toast.show(i18n.t('toasts.error_password_empty'), 'error');
DOMElements.loginForm.password.focus();
return;
}
loginButton.disabled = true;
loginButton.querySelector('span').textContent = '登录中...';
loginButton.querySelector('span').textContent = i18n.t('pages.login.logging_in_btn');
try {
const response = await fetch(LOGIN_API_URL, {
@ -67,23 +142,57 @@ document.addEventListener('DOMContentLoaded', () => {
});
const result = await response.json();
if (response.ok) {
toast.show('登录成功,正在跳转...', 'success');
toast.show(i18n.t('toasts.login_success'), 'success');
setTimeout(() => { window.location.href = '/'; }, 500);
} else {
throw new Error(result.error || `登录失败: ${response.status}`);
throw new Error(result.error || i18n.t('toasts.login_error_generic'));
}
} catch (error) {
toast.show(error.message, 'error');
loginButton.disabled = false;
loginButton.querySelector('span').textContent = '登录';
loginButton.querySelector('span').textContent = i18n.t('pages.login.login_btn');
}
}
function init() {
theme.init();
async function initApp() {
// 主题设置逻辑
const storedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.dataset.theme = storedTheme || (systemPrefersDark ? 'dark' : 'light');
// 初始化国际化
await i18n.init();
// 登录表单事件监听
if (DOMElements.loginForm) {
DOMElements.loginForm.addEventListener('submit', handleLogin);
}
// 语言切换按钮事件监听 (从第一个片段引入)
if (DOMElements.langSwitcherBtn) {
DOMElements.langSwitcherBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡, 防止立即触发document的点击事件
DOMElements.langOptionsList.classList.toggle('hidden');
});
}
// 语言选项列表事件监听 (从第一个片段引入)
if (DOMElements.langOptionsList) {
DOMElements.langOptionsList.addEventListener('click', async (e) => {
const target = e.target.closest('li[data-lang]'); // 查找最近的语言li元素
if (target) {
await i18n.loadLocale(target.dataset.lang); // 加载新语言
i18n.applyTranslations(); // 应用翻译
i18n.populateLangOptions(); // 更新语言选项列表的激活状态
DOMElements.langOptionsList.classList.add('hidden'); // 隐藏列表
}
});
}
// 文档点击事件, 用于点击外部时隐藏语言选项列表 (从第一个片段引入)
document.addEventListener('click', () => {
if (DOMElements.langOptionsList && !DOMElements.langOptionsList.classList.contains('hidden')) {
DOMElements.langOptionsList.classList.add('hidden');
}
});
}
init();
initApp();
});

View file

@ -1,8 +1,9 @@
// js/notifications.js - 提供Toast和Dialog两种通知
// 这个模块在初始化时需要知道容器的DOM元素
let toastContainer;
let dialogContainer;
let modalContainer; // 虽然在此文件中不直接使用, 但 init 中保留以示完整
let t; // 模块级翻译函数变量
function hideToast(toastElement) {
if (!toastElement) return;
@ -11,11 +12,14 @@ function hideToast(toastElement) {
}
export const notification = {
init: (toastEl, dialogEl) => {
init: (toastEl, dialogEl, modalEl, translator) => {
toastContainer = toastEl;
dialogContainer = dialogEl;
modalContainer = modalEl;
t = translator; // 保存从外部传入的翻译函数
},
toast: (message, type = 'info', duration = 3000) => {
if (!toastContainer) return;
const icons = { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle', warning: 'fa-exclamation-triangle' };
const iconClass = icons[type] || 'fa-info-circle';
const toastElement = document.createElement('div');
@ -29,28 +33,44 @@ export const notification = {
hideToast(toastElement);
});
},
confirm: (message) => {
confirm: (message, title = '', options = {}) => {
return new Promise(resolve => {
if (!dialogContainer || !t) {
// 如果模块未初始化, 提供一个浏览器默认的 confirm作为回退
console.warn('Notification module not initialized. Falling back to native confirm.');
resolve(window.confirm(message));
return;
}
// 使用 t 函数翻译按钮文本, 如果 options 中提供了自定义键, 则优先使用
const confirmText = options.confirmText || t('dialogs.confirm_btn');
const cancelText = options.cancelText || t('dialogs.cancel_btn');
const dialogHTML = `
<div class="dialog-box">
${title ? `<h3>${title}</h3>` : ''}
<p class="dialog-message">${message}</p>
<div class="dialog-actions">
<button class="btn btn-secondary" data-action="cancel">取消</button>
<button class="btn btn-primary" data-action="confirm">确定</button>
<button class="btn btn-secondary" data-action="cancel">${cancelText}</button>
<button class="btn btn-primary" data-action="confirm">${confirmText}</button>
</div>
</div>`;
dialogContainer.innerHTML = dialogHTML;
dialogContainer.classList.add('active');
const eventHandler = (e) => {
const actionButton = e.target.closest('[data-action]');
if (!actionButton) return;
closeDialog(actionButton.dataset.action === 'confirm');
};
const closeDialog = (result) => {
dialogContainer.removeEventListener('click', eventHandler);
dialogContainer.classList.remove('active');
setTimeout(() => { dialogContainer.innerHTML = ''; resolve(result); }, 200);
};
dialogContainer.addEventListener('click', eventHandler);
});
}

View file

@ -1,18 +1,14 @@
// js/settings.js - 设置页面的逻辑
import { theme, toast, activateNav } from './common.js';
import { initCaddyStatus } from './caddy.js'; // 导入 Caddy 状态模块
import { notification } from './notifications.js'; // 导入通知模块
const RESET_PWD_API_URL = '/v0/api/auth/resetpwd';
const LOGOUT_API_URL = '/v0/api/auth/logout';
import { initializePage } from './common.js';
import { api } from './api.js';
import { notification } from './notifications.js';
import { t, setLanguage, getCurrentLanguage } from './locale.js';
import { createCustomSelect } from './ui.js';
const DOMElements = {
resetForm: document.getElementById('reset-password-form'),
themeToggleInput: document.getElementById('theme-toggle-input'),
logoutBtn: document.getElementById('logout-btn'),
toastContainer: document.getElementById('toast-container'),
dialogContainer: document.getElementById('dialog-container'),
};
const resetButton = DOMElements.resetForm.querySelector('button[type="submit"]');
@ -20,81 +16,62 @@ async function handleResetPassword(e) {
e.preventDefault();
const newPassword = DOMElements.resetForm.new_password.value;
const confirmPassword = DOMElements.resetForm.confirm_new_password.value;
//保证字段均不为空, 用户名 密码 新密码
const currentPassword = DOMElements.resetForm.old_password.value;
const username = DOMElements.resetForm.username.value;
if (username === '') {
toast.show('用户名不能为空', 'error');
DOMElements.resetForm.username.focus();
if (!username || !currentPassword || !newPassword || !confirmPassword) {
notification.toast(t('toasts.error_all_fields_required'), 'error');
return;
}
if (currentPassword === '') {
toast.show('当前密码不能为空', 'error');
DOMElements.resetForm.old_password.focus();
return;
}
if (newPassword === '') {
notification.toast('新密码不能为空', 'error');
DOMElements.resetForm.new_password.focus();
return;
}
if (confirmPassword === '') {
notification.toast('确认新密码不能为空', 'error');
DOMElements.resetForm.confirm_new_password.focus();
return;
}
if (newPassword !== confirmPassword) {
notification.toast('新密码与确认密码不匹配', 'error');
notification.toast(t('toasts.init_error_mismatch'), 'error');
return;
}
if (newPassword.length < 8) {
notification.toast('新密码长度至少为8位', 'error');
notification.toast(t('toasts.init_error_short'), 'error');
return;
}
resetButton.disabled = true;
resetButton.querySelector('span').textContent = '重置中...';
const formData = new FormData(DOMElements.resetForm);
resetButton.querySelector('span').textContent = t('pages.settings.resetting_password_btn');
try {
const response = await fetch(RESET_PWD_API_URL, {
method: 'POST',
body: new URLSearchParams(formData),
});
const result = await response.json();
if (response.ok) {
notification.toast('密码重置成功!请重新登录。', 'success');
setTimeout(() => { window.location.href = LOGOUT_API_URL; }, 1500);
} else {
throw new Error(result.error || '重置密码失败');
}
const result = await api.post('/auth/resetpwd', new URLSearchParams(new FormData(DOMElements.resetForm)));
notification.toast(t('toasts.pwd_reset_success'), 'success');
setTimeout(() => { window.location.href = '/v0/api/auth/logout'; }, 1500);
} catch (error) {
notification.toast(error.message, 'error');
notification.toast(`${t('common.error_prefix')}: ${error.message}`, 'error');
resetButton.disabled = false;
resetButton.querySelector('span').textContent = '重置密码';
resetButton.querySelector('span').textContent = t('pages.settings.reset_password_btn');
}
}
async function handleLogout() {
if (await notification.confirm('您确定要退出登录吗?')) {
notification.toast('正在退出...', 'info');
setTimeout(() => { window.location.href = LOGOUT_API_URL; }, 500);
if (await notification.confirm(t('dialogs.logout_msg'))) {
notification.toast(t('toasts.logout_processing'), 'info');
setTimeout(() => { window.location.href = '/v0/api/auth/logout'; }, 500);
}
}
function init() {
theme.init(DOMElements.themeToggleInput);
notification.init(DOMElements.toastContainer, DOMElements.dialogContainer);
activateNav('settings');
initCaddyStatus(); // 初始化通用Caddy状态检查
// 页面特有的初始化逻辑
function pageInit() {
const langOptions = { 'en': 'English', 'zh-CN': '简体中文' };
const langSelectOptions = Object.keys(langOptions).map(key => ({ name: langOptions[key], value: key }));
createCustomSelect('select-language', langSelectOptions, (selectedValue) => {
setLanguage(selectedValue);
});
const langSelect = document.getElementById('select-language');
if (langSelect) {
const currentLangName = langOptions[getCurrentLanguage()];
const selectedDiv = langSelect.querySelector('.select-selected');
if (selectedDiv) selectedDiv.textContent = currentLangName;
}
DOMElements.resetForm.addEventListener('submit', handleResetPassword);
DOMElements.logoutBtn.addEventListener('click', handleLogout);
}
init();
// 使用通用初始化函数
initializePage({ pageId: 'settings', pageInit: pageInit });

View file

@ -1,5 +1,13 @@
// js/ui.js - 管理所有与UI渲染和DOM操作相关的函数
// 模块级私有变量, 用于存储翻译函数
let t;
// 新增: 初始化函数, 用于接收翻译函数
export function initUI(translator) {
t = translator;
}
export const DOMElements = {
sidebar: document.getElementById('sidebar'),
menuToggleBtn: document.getElementById('menu-toggle-btn'),
@ -45,14 +53,14 @@ export function switchView(viewToShow) {
export function renderConfigList(filenames) {
DOMElements.configListContainer.innerHTML = '';
if (!filenames || filenames.length === 0) {
DOMElements.configListContainer.innerHTML = '<p>还没有任何配置,请创建一个。</p>';
DOMElements.configListContainer.innerHTML = `<p>${t('configs.no_configs')}</p>`;
return;
}
filenames.forEach(filename => {
const item = document.createElement('li');
item.className = 'config-item';
item.dataset.filename = filename;
item.innerHTML = `<span class="config-item-name">${filename}</span><div class="config-item-actions"><button class="btn-icon edit-btn" title="编辑"><i class="fa-solid fa-pen-to-square"></i></button><button class="btn-icon delete-btn" title="删除"><i class="fa-solid fa-trash-can"></i></button></div>`;
item.innerHTML = `<span class="config-item-name">${filename}</span><div class="config-item-actions"><button class="btn-icon edit-btn" title="${t('common.edit')}"><i class="fa-solid fa-pen-to-square"></i></button><button class="btn-icon delete-btn" title="${t('common.delete')}"><i class="fa-solid fa-trash-can"></i></button></div>`;
DOMElements.configListContainer.appendChild(item);
});
}
@ -61,21 +69,21 @@ export function addKeyValueInput(container, keyName, valueName, key = '', value
const div = document.createElement('div');
div.className = 'header-entry';
div.innerHTML = `
<input type="text" name="${keyName}" placeholder="Key" value="${key}">
<input type="text" name="${valueName}" placeholder="Value" value="${value}">
<button type="button" class="btn-icon" onclick="this.parentElement.remove()" title="移除此条目">
<input type="text" name="${keyName}" placeholder="${t('form.key_placeholder')}" value="${key}">
<input type="text" name="${valueName}" placeholder="${t('form.value_placeholder')}" value="${value}">
<button type="button" class="btn-icon" onclick="this.parentElement.remove()" title="${t('common.remove_item')}">
<i class="fa-solid fa-xmark"></i>
</button>`;
container.appendChild(div);
}
export function addSingleInput(container, inputName, placeholder, value = '') {
export function addSingleInput(container, inputName, placeholderKey, 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="移除此上游">
<input type="text" name="${inputName}" placeholder="${t(placeholderKey)}" value="${value}">
<button type="button" class="btn-icon" onclick="this.parentElement.remove()" title="${t('common.remove_upstream')}">
<i class="fa-solid fa-xmark"></i>
</button>`;
container.appendChild(div);
@ -97,7 +105,7 @@ export function fillForm(config, originalFilename) {
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);
addSingleInput(DOMElements.multiUpstreamContainer, 'upstream_servers', 'form.upstream_server_placeholder', server);
});
}
DOMElements.upstreamHeadersContainer.innerHTML = '';
@ -137,21 +145,22 @@ export function updateCaddyStatusView(status, handlers) {
const dot = DOMElements.caddyStatusIndicator.querySelector('.status-dot');
const text = DOMElements.caddyStatusIndicator.querySelector('.status-text');
const buttonContainer = DOMElements.caddyActionButtonContainer;
if (!dot || !text || !buttonContainer) return;
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));
statusText = t('status.running'); dotClass = 'running';
buttonContainer.appendChild(createButton(t('caddy.reload_btn'), 'btn-warning', handleReloadCaddy));
buttonContainer.appendChild(createButton(t('caddy.stop_btn'), 'btn-danger', handleStopCaddy));
break;
case 'stopped':
statusText = '已停止'; dotClass = 'stopped';
buttonContainer.appendChild(createButton('启动 Caddy', 'btn-success', handleStartCaddy));
statusText = t('status.stopped'); dotClass = 'stopped';
buttonContainer.appendChild(createButton(t('caddy.start_btn'), 'btn-success', handleStartCaddy));
break;
case 'checking': statusText = '检查中...'; dotClass = 'checking'; break;
default: statusText = '状态未知'; dotClass = 'error'; break;
case 'checking': statusText = t('status.checking'); dotClass = 'checking'; break;
default: statusText = t('status.unknown'); dotClass = 'error'; break;
}
text.textContent = statusText;
dot.classList.add(dotClass);
@ -177,24 +186,21 @@ export function updateSegmentedControl(activeButton) {
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); // 安全退出
if (!modalContainer) return resolve(null);
const presetItems = presets.map(p => `
<li data-preset-id="${p.id}">
<strong>${p.name}</strong>
<p>${p.description}</p>
<strong>${t(p.name_key) || p.name}</strong>
<p>${t(p.desc_key) || p.description}</p>
</li>
`).join('');
const modalHTML = `
<div class="modal-overlay"></div>
<div class="modal-box">
<header class="modal-header">
<h3>从预设填充</h3>
<h3>${t('form.fill_from_preset')}</h3>
<button class="btn-icon" data-modal-close><i class="fa-solid fa-xmark"></i></button>
</header>
<div class="modal-content">
@ -202,29 +208,22 @@ export function createPresetSelectionModal(presets) {
</div>
</div>
`;
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);
setTimeout(() => { modalContainer.innerHTML = ''; resolve(value); }, 300);
};
const eventHandler = (e) => {
if (e.target.classList.contains('modal-overlay') || e.target.closest('[data-modal-close]')) {
cleanupAndResolve(null); // 用户取消
cleanupAndResolve(null);
}
const listItem = e.target.closest('li[data-preset-id]');
if (listItem) {
cleanupAndResolve(listItem.dataset.presetId); // 用户选择
cleanupAndResolve(listItem.dataset.presetId);
}
};
modalContainer.addEventListener('click', eventHandler);
});
}
@ -233,28 +232,30 @@ 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 = `<div class="select-selected"></div><div class="select-items"></div><input type="hidden" name="${inputName}">`;
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 = '无可用选项';
selectedDiv.textContent = t('common.no_options');
return;
}
options.forEach((option, index) => {
const item = document.createElement('div');
item.textContent = option;
item.dataset.value = option;
const optionText = typeof option === 'object' ? option.name : option;
const optionValue = typeof option === 'object' ? option.value : option;
item.textContent = optionText;
item.dataset.value = optionValue;
if (index === 0) {
selectedDiv.textContent = option;
hiddenInput.value = option;
selectedDiv.textContent = optionText;
hiddenInput.value = optionValue;
}
item.addEventListener('click', function(e) {
selectedDiv.textContent = this.textContent;
@ -266,6 +267,7 @@ export function createCustomSelect(containerId, options, onSelect) {
});
itemsDiv.appendChild(item);
});
selectedDiv.addEventListener('click', (e) => {
e.stopPropagation();
document.querySelectorAll('.select-items.select-show').forEach(openSelect => {
@ -277,6 +279,7 @@ export function createCustomSelect(containerId, options, onSelect) {
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');