caddydash/frontend/js/init.js
2025-07-01 07:04:39 +08:00

215 lines
No EOL
9.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// js/init.js - 初始化页面的独立逻辑
document.addEventListener('DOMContentLoaded', () => {
const DOMElements = {
initForm: document.getElementById('init-form'),
toastContainer: document.getElementById('toast-container'),
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 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.init.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: 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 ${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(() => this._hide(toastElement), duration);
toastElement.querySelector('[data-toast-close]').addEventListener('click', () => {
clearTimeout(timeoutId);
this._hide(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();
if (username === '') {
toast.show(i18n.t('toasts.error_username_empty'), 'error');
DOMElements.initForm.username.focus();
return;
}
if (password === '') {
toast.show(i18n.t('toasts.error_password_empty'), 'error');
DOMElements.initForm.password.focus();
return;
}
if (password !== confirmPassword) {
toast.show(i18n.t('toasts.init_error_mismatch'), 'error');
DOMElements.initForm.confirm_password.focus();
return;
}
if (password.length < PASSWORD_MIN_LENGTH) {
toast.show(i18n.t('toasts.init_error_short', { minLength: PASSWORD_MIN_LENGTH }), 'error');
DOMElements.initForm.password.focus();
return;
}
initButton.disabled = true;
initButton.querySelector('span').textContent = i18n.t('pages.init.setting_up_btn');
try {
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();
if (response.ok) {
toast.show(i18n.t('toasts.init_success'), 'success');
setTimeout(() => { window.location.href = '/login.html'; }, 1500);
} else {
throw new Error(result.error || i18n.t('toasts.init_error_generic'));
}
} catch (error) {
toast.show(error.message, 'error');
initButton.disabled = false;
initButton.querySelector('span').textContent = i18n.t('pages.init.setup_btn');
}
}
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.initForm.addEventListener('submit', handleInitSubmit);
}
// 语言切换按钮事件监听 (从第一个片段引入)
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();
});