mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-08-08 15:13:50 +08:00
feat(i18n): full i18n for all pages + sidebar lang switcher + zh-TW locale
- All pages now use t() for internationalization - Sidebar footer: searchable upward dropdown language switcher - Generated zh-TW.json (Traditional Chinese) via gen-locales.cjs - CSS for lang switcher with mobile/collapsed sidebar support - Removed language toggle from settings page
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import { toast } from '../components/toast.js'
|
||||
import { statusIcon } from '../lib/icons.js'
|
||||
import { t } from '../lib/i18n.js'
|
||||
|
||||
const isTauri = !!window.__TAURI_INTERNALS__
|
||||
let _tauriApi = null
|
||||
@@ -26,10 +27,10 @@ async function apiCall(cmd, args = {}) {
|
||||
return result
|
||||
}
|
||||
if (cmd === 'auth_change_password') {
|
||||
if (cfg.accessPassword && args.oldPassword !== cfg.accessPassword) throw new Error('当前密码错误')
|
||||
if (cfg.accessPassword && args.oldPassword !== cfg.accessPassword) throw new Error(t('security.wrongPassword'))
|
||||
const weakErr = checkPasswordStrengthLocal(args.newPassword)
|
||||
if (weakErr) throw new Error(weakErr)
|
||||
if (args.newPassword === cfg.accessPassword) throw new Error('新密码不能与旧密码相同')
|
||||
if (args.newPassword === cfg.accessPassword) throw new Error(t('security.pwSameAsOld'))
|
||||
cfg.accessPassword = args.newPassword
|
||||
delete cfg.mustChangePassword
|
||||
delete cfg.ignoreRisk
|
||||
@@ -63,27 +64,27 @@ async function apiCall(cmd, args = {}) {
|
||||
}
|
||||
|
||||
function checkPasswordStrengthLocal(pw) {
|
||||
if (!pw || pw.length < 6) return '密码至少 6 位'
|
||||
if (pw.length > 64) return '密码不能超过 64 位'
|
||||
if (/^\d+$/.test(pw)) return '密码不能是纯数字'
|
||||
if (!pw || pw.length < 6) return t('security.pwMin6')
|
||||
if (pw.length > 64) return t('security.pwMax64')
|
||||
if (/^\d+$/.test(pw)) return t('security.pwNoDigitOnly')
|
||||
const weak = ['123456', '654321', 'password', 'admin', 'qwerty', 'abc123', '111111', '000000', 'letmein', 'welcome', 'clawpanel', 'openclaw']
|
||||
if (weak.includes(pw.toLowerCase())) return '密码太常见,请换一个更安全的密码'
|
||||
if (weak.includes(pw.toLowerCase())) return t('security.pwTooCommon')
|
||||
return null
|
||||
}
|
||||
|
||||
function strengthLevel(pw) {
|
||||
if (!pw) return { level: 0, text: '', color: '' }
|
||||
if (pw.length < 6) return { level: 1, text: '太短', color: 'var(--error)' }
|
||||
if (/^\d+$/.test(pw)) return { level: 1, text: '纯数字太弱', color: 'var(--error)' }
|
||||
if (pw.length < 6) return { level: 1, text: t('security.strengthTooShort'), color: 'var(--error)' }
|
||||
if (/^\d+$/.test(pw)) return { level: 1, text: t('security.strengthDigitOnly'), color: 'var(--error)' }
|
||||
let score = 0
|
||||
if (pw.length >= 8) score++
|
||||
if (pw.length >= 12) score++
|
||||
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score++
|
||||
if (/\d/.test(pw)) score++
|
||||
if (/[^a-zA-Z0-9]/.test(pw)) score++
|
||||
if (score <= 1) return { level: 2, text: '一般', color: 'var(--warning)' }
|
||||
if (score <= 3) return { level: 3, text: '良好', color: 'var(--primary)' }
|
||||
return { level: 4, text: '强', color: 'var(--success)' }
|
||||
if (score <= 1) return { level: 2, text: t('security.strengthFair'), color: 'var(--warning)' }
|
||||
if (score <= 3) return { level: 3, text: t('security.strengthGood'), color: 'var(--primary)' }
|
||||
return { level: 4, text: t('security.strengthStrong'), color: 'var(--success)' }
|
||||
}
|
||||
|
||||
export async function render() {
|
||||
@@ -91,7 +92,7 @@ export async function render() {
|
||||
page.className = 'page'
|
||||
|
||||
page.innerHTML = `
|
||||
<div class="page-header"><h1>安全设置</h1></div>
|
||||
<div class="page-header"><h1>${t('security.title')}</h1></div>
|
||||
<div id="security-content">
|
||||
<div class="config-section loading-placeholder" style="height:120px"></div>
|
||||
</div>
|
||||
@@ -107,7 +108,7 @@ async function loadStatus(page) {
|
||||
const status = await apiCall('auth_status')
|
||||
renderContent(container, status)
|
||||
} catch (e) {
|
||||
container.innerHTML = `<div class="config-section"><p style="color:var(--error)">加载失败: ${e.message}</p></div>`
|
||||
container.innerHTML = `<div class="config-section"><p style="color:var(--error)">${t('security.loadFailed')}: ${e.message}</p></div>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,21 +118,21 @@ function renderContent(container, status) {
|
||||
// 当前状态
|
||||
const stateIcon = status.hasPassword ? statusIcon('ok', 20) : statusIcon('warn', 20)
|
||||
const stateText = status.hasPassword
|
||||
? (status.mustChangePassword ? '使用默认密码(需修改)' : '已设置自定义密码')
|
||||
: (status.ignoreRisk ? '无视风险模式(无密码)' : '未设置密码')
|
||||
? (status.mustChangePassword ? t('security.stateDefault') : t('security.stateCustom'))
|
||||
: (status.ignoreRisk ? t('security.stateIgnoreRisk') : t('security.stateNone'))
|
||||
const stateColor = status.hasPassword && !status.mustChangePassword ? 'var(--success)' : 'var(--warning)'
|
||||
|
||||
html += `
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">访问密码状态</div>
|
||||
<div class="config-section-title">${t('security.passwordStatus')}</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:12px 16px;background:var(--bg-tertiary);border-radius:var(--radius-sm);border-left:3px solid ${stateColor}">
|
||||
<span style="font-size:20px">${stateIcon}</span>
|
||||
<div>
|
||||
<div style="font-weight:600;color:var(--text-primary)">${stateText}</div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-tertiary);margin-top:2px">
|
||||
${status.hasPassword
|
||||
? (isTauri ? '每次打开应用需输入密码' : '远程访问需输入密码才能进入面板')
|
||||
: (isTauri ? '任何人打开应用即可使用' : '任何人都可以直接访问面板')}
|
||||
? (isTauri ? t('security.tauriHasPassword') : t('security.webHasPassword'))
|
||||
: (isTauri ? t('security.tauriNoPassword') : t('security.webNoPassword'))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,26 +142,26 @@ function renderContent(container, status) {
|
||||
// 修改密码区域
|
||||
html += `
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">${status.hasPassword ? '修改密码' : '设置密码'}</div>
|
||||
<div class="config-section-title">${status.hasPassword ? t('security.changePassword') : t('security.setPassword')}</div>
|
||||
<form id="form-change-pw" style="max-width:400px">
|
||||
${status.hasPassword ? `
|
||||
<div style="margin-bottom:12px">
|
||||
<label style="display:block;font-size:var(--font-size-xs);color:var(--text-tertiary);margin-bottom:4px">当前密码</label>
|
||||
<input type="password" id="sec-old-pw" class="form-input" placeholder="输入当前密码" autocomplete="current-password" style="width:100%"
|
||||
<label style="display:block;font-size:var(--font-size-xs);color:var(--text-tertiary);margin-bottom:4px">${t('security.currentPassword')}</label>
|
||||
<input type="password" id="sec-old-pw" class="form-input" placeholder="${t('security.currentPasswordPlaceholder')}" autocomplete="current-password" style="width:100%"
|
||||
${status.defaultPassword ? `value="${status.defaultPassword}"` : ''}>
|
||||
${status.defaultPassword ? '<div style="font-size:11px;color:var(--text-tertiary);margin-top:4px">已自动填充默认密码,直接设置新密码即可</div>' : ''}
|
||||
${status.defaultPassword ? `<div style="font-size:11px;color:var(--text-tertiary);margin-top:4px">${t('security.defaultFilled')}</div>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
<div style="margin-bottom:12px">
|
||||
<label style="display:block;font-size:var(--font-size-xs);color:var(--text-tertiary);margin-bottom:4px">新密码</label>
|
||||
<input type="password" id="sec-new-pw" class="form-input" placeholder="至少 6 位,不能纯数字" autocomplete="new-password" style="width:100%">
|
||||
<label style="display:block;font-size:var(--font-size-xs);color:var(--text-tertiary);margin-bottom:4px">${t('security.newPassword')}</label>
|
||||
<input type="password" id="sec-new-pw" class="form-input" placeholder="${t('security.newPasswordPlaceholder')}" autocomplete="new-password" style="width:100%">
|
||||
<div id="pw-strength" style="margin-top:6px;display:flex;align-items:center;gap:8px;min-height:20px"></div>
|
||||
</div>
|
||||
<div style="margin-bottom:16px">
|
||||
<label style="display:block;font-size:var(--font-size-xs);color:var(--text-tertiary);margin-bottom:4px">确认新密码</label>
|
||||
<input type="password" id="sec-confirm-pw" class="form-input" placeholder="再次输入新密码" autocomplete="new-password" style="width:100%">
|
||||
<label style="display:block;font-size:var(--font-size-xs);color:var(--text-tertiary);margin-bottom:4px">${t('security.confirmPassword')}</label>
|
||||
<input type="password" id="sec-confirm-pw" class="form-input" placeholder="${t('security.confirmPasswordPlaceholder')}" autocomplete="new-password" style="width:100%">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">${status.hasPassword ? '确认修改' : '设置密码'}</button>
|
||||
<button type="submit" class="btn btn-primary btn-sm">${status.hasPassword ? t('security.confirmChange') : t('security.setPassword')}</button>
|
||||
<span id="change-pw-msg" style="margin-left:12px;font-size:var(--font-size-xs)"></span>
|
||||
</form>
|
||||
</div>
|
||||
@@ -171,15 +172,15 @@ function renderContent(container, status) {
|
||||
<div class="config-section">
|
||||
<div class="config-section-title" style="display:flex;align-items:center;gap:6px">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
无视风险模式
|
||||
${t('security.ignoreRiskTitle')}
|
||||
</div>
|
||||
<div style="padding:12px 16px;background:${status.ignoreRisk ? 'rgba(239,68,68,0.08)' : 'var(--bg-tertiary)'};border-radius:var(--radius-sm);border:1px solid ${status.ignoreRisk ? 'rgba(239,68,68,0.2)' : 'var(--border-primary)'}">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px">
|
||||
<div>
|
||||
<div style="font-weight:500;color:var(--text-primary)">关闭密码保护</div>
|
||||
<div style="font-weight:500;color:var(--text-primary)">${t('security.ignoreRiskLabel')}</div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-secondary);margin-top:4px;line-height:1.5">
|
||||
开启后任何人都可以直接访问面板,无需输入密码。<br>
|
||||
<strong style="color:var(--error)">仅建议在受信任的内网环境中使用。</strong>
|
||||
${t('security.ignoreRiskDesc')}<br>
|
||||
<strong style="color:var(--error)">${t('security.ignoreRiskWarn')}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
@@ -189,13 +190,13 @@ function renderContent(container, status) {
|
||||
</div>
|
||||
</div>
|
||||
<div id="ignore-risk-confirm" style="display:none;margin-top:12px;padding:12px 16px;background:rgba(239,68,68,0.06);border-radius:var(--radius-sm);border:1px solid rgba(239,68,68,0.15)">
|
||||
<p style="font-size:var(--font-size-sm);color:var(--error);font-weight:600;margin-bottom:8px">确认关闭密码保护?</p>
|
||||
<p style="font-size:var(--font-size-sm);color:var(--error);font-weight:600;margin-bottom:8px">${t('security.ignoreRiskConfirmTitle')}</p>
|
||||
<p style="font-size:var(--font-size-xs);color:var(--text-secondary);margin-bottom:12px;line-height:1.5">
|
||||
关闭后,<strong>任何能访问此服务器 IP 和端口的人</strong>都可以直接进入管理面板,查看和修改你的 AI 配置。
|
||||
${t('security.ignoreRiskConfirmDesc')}
|
||||
</p>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn btn-sm" id="btn-confirm-ignore" style="background:var(--error);color:#fff;border:none">我了解风险,确认关闭</button>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-cancel-ignore">取消</button>
|
||||
<button class="btn btn-sm" id="btn-confirm-ignore" style="background:var(--error);color:#fff;border:none">${t('security.ignoreRiskConfirmBtn')}</button>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-cancel-ignore">${t('common.cancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,16 +232,16 @@ function bindSecurityEvents(container, status) {
|
||||
const msgEl = container.querySelector('#change-pw-msg')
|
||||
const btn = form.querySelector('button[type="submit"]')
|
||||
|
||||
if (newPw !== confirmPw) { msgEl.textContent = '两次输入的密码不一致'; msgEl.style.color = 'var(--error)'; return }
|
||||
if (newPw !== confirmPw) { msgEl.textContent = t('security.passwordMismatch'); msgEl.style.color = 'var(--error)'; return }
|
||||
|
||||
btn.disabled = true
|
||||
btn.textContent = '提交中...'
|
||||
btn.textContent = t('security.submitting')
|
||||
msgEl.textContent = ''
|
||||
try {
|
||||
await apiCall('auth_change_password', { oldPassword: oldPw, newPassword: newPw })
|
||||
msgEl.textContent = '密码修改成功'
|
||||
msgEl.textContent = t('security.passwordChanged')
|
||||
msgEl.style.color = 'var(--success)'
|
||||
toast('密码已更新', 'success')
|
||||
toast(t('security.passwordUpdated'), 'success')
|
||||
// 清除默认密码横幅
|
||||
sessionStorage.removeItem('clawpanel_must_change_pw')
|
||||
const banner = document.getElementById('pw-change-banner')
|
||||
@@ -250,7 +251,7 @@ function bindSecurityEvents(container, status) {
|
||||
msgEl.textContent = err.message
|
||||
msgEl.style.color = 'var(--error)'
|
||||
btn.disabled = false
|
||||
btn.textContent = status.hasPassword ? '确认修改' : '设置密码'
|
||||
btn.textContent = status.hasPassword ? t('security.confirmChange') : t('security.setPassword')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -283,12 +284,12 @@ async function handleIgnoreRisk(container, enable) {
|
||||
try {
|
||||
await apiCall('auth_ignore_risk', { enable })
|
||||
if (enable) {
|
||||
toast('已开启无视风险模式,密码保护已关闭', 'warning')
|
||||
toast(t('security.ignoreRiskEnabled'), 'warning')
|
||||
} else {
|
||||
toast('无视风险模式已关闭,请设置新密码', 'info')
|
||||
toast(t('security.ignoreRiskDisabled'), 'info')
|
||||
}
|
||||
setTimeout(() => loadStatus(container.closest('.page')), 500)
|
||||
} catch (e) {
|
||||
toast('操作失败: ' + e.message, 'error')
|
||||
toast(t('security.operationFailed') + ': ' + e.message, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user