add i18n step1
This commit is contained in:
parent
34d553a890
commit
79e3db6078
23 changed files with 2309 additions and 450 deletions
|
|
@ -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 });
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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 });
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue