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