diff --git a/components/ts_webui/web/css/style.css b/components/ts_webui/web/css/style.css index ad9adc7..521e96a 100644 --- a/components/ts_webui/web/css/style.css +++ b/components/ts_webui/web/css/style.css @@ -192,6 +192,7 @@ body { .footer { height: var(--footer-height); line-height: var(--footer-height); + margin-top: -14px; background: transparent; border-top: none; text-align: center; @@ -1686,10 +1687,18 @@ button.btn-gray:hover, display: flex; justify-content: space-between; align-items: center; - margin-bottom: 24px; + margin-bottom: 12px; /* 与 Device Panel 等 section-header 一致 */ flex-wrap: wrap; gap: 12px; } +/* 系统页 LED 区块:上下间距相等,使标题+按钮在灰色顶线与蓝色内容线间垂直居中 */ +.section .led-page-header { + margin-top: -12px; /* 24px padding → 12px,与 margin-bottom 一致 */ +} +/* 独立 LED 页:main 有 20px padding,同样实现上下对称 */ +.page-led .led-page-header { + margin-top: -8px; /* 20px → 12px,与 margin-bottom 一致 */ +} .led-page-header h1 { margin: 0; @@ -5564,6 +5573,18 @@ button.btn-gray:hover, font-size: 0.85em; } +/* 操作列图标按钮:统一为正方形,避免下载/删除按钮比前两个宽 */ +.page-automation .data-table td .btn.btn-sm.btn-icon-square, +.page-automation .data-table td .btn.btn-sm.btn-danger.btn-icon-square { + width: 32px; + min-width: 32px; + height: 32px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} + /* 状态徽章 */ .status-badge { display: inline-block; @@ -5774,7 +5795,7 @@ button.btn-gray:hover, #add-rule-modal .form-row.three-col { display: flex; flex-wrap: nowrap; - align-items: flex-start; + align-items: center; gap: 12px; } @@ -5784,6 +5805,13 @@ button.btn-gray:hover, min-width: 0; } +#add-rule-modal .form-row.three-col .form-group-logic select.input, +#add-rule-modal .form-row.three-col .form-group input[type="number"] { + height: 38px; + min-height: 38px; + box-sizing: border-box; +} + #add-rule-modal .form-row.three-col .form-group:nth-of-type(2) { flex: 0 0 140px; min-width: 0; @@ -5791,6 +5819,18 @@ button.btn-gray:hover, #add-rule-modal .form-row.three-col .checkbox-label { flex: 0 0 auto; + margin-left: 16px; + margin-bottom: 0; +} + +/* 冷却时间与条件逻辑样式统一:隐藏 number 的默认 spinner,与 select 下拉视觉一致 */ +#add-rule-modal input[type="number"] { + -moz-appearance: textfield; +} +#add-rule-modal input[type="number"]::-webkit-outer-spin-button, +#add-rule-modal input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; } /* 条件行 */ @@ -7238,6 +7278,18 @@ button.btn-gray:hover, margin-bottom: 20px; } +/* 系统页:Resource Monitor / Device Panel / Fan Control / LED 卡片间距统一为 20px */ +.page-system .panel-row { + margin-top: 20px; + margin-bottom: 20px; +} +.page-system .panel-row .section { + margin-top: 0; /* 由 panel-row margin-top 提供与上方卡片的间距 */ +} +.page-system .panel-row + .section { + margin-top: 20px; /* LED 与 panel-row 的间距,覆盖 .section 默认 16px */ +} + @media (max-width: 1200px) { .panel-row { grid-template-columns: 1fr; diff --git a/components/ts_webui/web/fonts/remixicon.css b/components/ts_webui/web/fonts/remixicon.css index aace1d6..69bba4d 100644 --- a/components/ts_webui/web/fonts/remixicon.css +++ b/components/ts_webui/web/fonts/remixicon.css @@ -92,6 +92,7 @@ .ri-bar-chart-line:before { content: "\ea9e"; } .ri-search-line:before { content: "\f0d1"; } .ri-stop-line:before { content: "\f1a1"; } +.ri-stop-fill:before { content: "\f1a0"; } .ri-play-line:before { content: "\f00b"; } .ri-settings-line:before { content: "\f0ee"; } .ri-settings-3-line:before { content: "\f0e6"; } diff --git a/components/ts_webui/web/index.html b/components/ts_webui/web/index.html index 834b11f..eae73a8 100644 --- a/components/ts_webui/web/index.html +++ b/components/ts_webui/web/index.html @@ -88,7 +88,7 @@
-

Loading

+

Loading

@@ -114,7 +114,7 @@

登录 Tia
+ data-i18n-placeholder="login.passwordPlaceholder" placeholder="请输入密码" autocomplete="current-password">
@@ -351,6 +351,8 @@

电压保护设置

(function loadInitialLang() { i18n.init(); var cur = i18n.getLanguage(); + var nameEl = document.getElementById('lang-name'); + if (nameEl) nameEl.textContent = cur === 'zh-CN' ? '\u4e2d\u6587' : 'English'; var s = document.createElement('script'); s.src = '/js/lang/' + cur + '.js'; s.onload = function() { diff --git a/components/ts_webui/web/js/app.js b/components/ts_webui/web/js/app.js index 6a5dc29..c6bd49e 100644 --- a/components/ts_webui/web/js/app.js +++ b/components/ts_webui/web/js/app.js @@ -433,7 +433,7 @@ function handleEvent(msg) { if (typeof window.renderFilteredLogs === 'function') { window.renderFilteredLogs(); } - showToast(`加载了 ${logs.length} 条历史日志`, 'success'); + showToast(typeof t === 'function' ? t('toast.logsLoadedCount', { count: logs.length }) : `加载了 ${logs.length} 条历史日志`, 'success'); } // 终端页面的日志模态框 @@ -480,11 +480,11 @@ function handlePowerEvent(msg) { // 显示警告 if (state === 'LOW_VOLTAGE' || state === 'SHUTDOWN') { - showToast(`低电压警告: ${voltage}V (${countdown}s)`, 'warning', 5000); + showToast(typeof t === 'function' ? t('system.lowVoltageWarning', { voltage, countdown }) : `低电压警告: ${voltage}V (${countdown}s)`, 'warning', 5000); } else if (state === 'PROTECTED') { - showToast(`电压保护已触发`, 'error', 10000); + showToast(typeof t === 'function' ? t('system.voltageProtectionTriggered') : '电压保护已触发', 'error', 10000); } else if (state === 'RECOVERY') { - showToast(`电压恢复中: ${voltage}V`, 'info', 3000); + showToast(typeof t === 'function' ? t('system.voltageRecovering', { voltage }) : `电压恢复中: ${voltage}V`, 'info', 3000); } } @@ -714,12 +714,15 @@ async function loadSystemPage() { `; - // 初始加载 + // 初始加载(不含快捷操作,避免 ssh.commands.list 慢响应阻塞) await refreshSystemPageOnce(); - // 加载数据监控面板 + // 加载数据监控面板(优先执行,确保组件能实时获取变量数据) await initDataWidgets(); + // 快捷操作延迟 2 秒后台加载,避免与 initDataWidgets 的首次变量刷新竞争 API + setTimeout(() => void refreshQuickActions(), 2000); + // 订阅 WebSocket 实时更新 - 使用聚合订阅(system.dashboard) if (subscriptionManager) { subscriptionManager.subscribe('system.dashboard', (msg) => { @@ -819,8 +822,7 @@ async function refreshSystemPageOnce() { // LED 设备 await refreshSystemLeds(); - // 快捷操作 - await refreshQuickActions(); + // 快捷操作已移至 loadSystemPage 末尾后台执行,避免阻塞 initDataWidgets // USB Mux 状态 await refreshUsbMuxStatus(); @@ -1287,7 +1289,7 @@ function updateMemoryInfo(data) { document.getElementById('psram-text').textContent = `${formatBytes(psramUsed)} / ${formatBytes(psramTotal)} (${psramPercent}%)`; } else { - document.getElementById('psram-text').textContent = '不可用'; + document.getElementById('psram-text').textContent = typeof t === 'function' ? t('otaPage.psramUnavailable') : '不可用'; } } @@ -1315,7 +1317,7 @@ function updateCpuInfo(data) { if (data.total_usage !== undefined) { const avgUsage = Math.round(data.total_usage); - html += `

平均: ${avgUsage}%

`; + html += `

${typeof t === 'function' ? t('common.average') : '平均'}: ${avgUsage}%

`; } container.innerHTML = html; @@ -1516,7 +1518,7 @@ async function applyTestTemp() { const temp = parseFloat(input?.value); if (isNaN(temp) || temp < 0 || temp > 100) { - showToast('请输入有效温度 (0-100°C)', 'warning'); + showToast((typeof t === 'function' ? t('fan.enterValidTemp') : '请输入有效温度 (0-100°C)'), 'warning'); return; } @@ -1525,15 +1527,15 @@ async function applyTestTemp() { const result = await api.call('temp.manual', { temperature: temp }); if (result.code === 0) { - showToast(`测试温度已设置为 ${temp}°C`, 'success'); + showToast((typeof t === 'function' ? t('fan.testTempSet', { temp }) : `测试温度已设置为 ${temp}°C`), 'success'); // 刷新风扇状态 await refreshFans(); } else { - showToast(`设置失败: ${result.message}`, 'error'); + showToast((typeof t === 'function' ? t('fan.setFailed', { msg: result.message }) : `设置失败: ${result.message}`), 'error'); } } catch (e) { console.error('设置测试温度失败:', e); - showToast(`设置失败: ${e.message}`, 'error'); + showToast((typeof t === 'function' ? t('fan.setFailed', { msg: e.message }) : `设置失败: ${e.message}`), 'error'); } } @@ -1546,16 +1548,16 @@ async function clearTestTemp() { const result = await api.call('temp.select', { source: 'variable' }); if (result.code === 0) { - showToast('测试温度已清除,恢复正常模式', 'success'); + showToast((typeof t === 'function' ? t('fan.testTempCleared') : '测试温度已清除,恢复正常模式'), 'success'); document.getElementById('fan-test-temp').value = ''; // 刷新风扇状态 await refreshFans(); } else { - showToast(`清除失败: ${result.message}`, 'error'); + showToast((typeof t === 'function' ? t('fan.clearFailed', { msg: result.message }) : `清除失败: ${result.message}`), 'error'); } } catch (e) { console.error('清除测试温度失败:', e); - showToast(`清除失败: ${e.message}`, 'error'); + showToast((typeof t === 'function' ? t('fan.clearFailed', { msg: e.message }) : `清除失败: ${e.message}`), 'error'); } } @@ -1567,16 +1569,16 @@ function hideServicesModal() { async function setFanSpeed(id, speed) { try { await api.fanSet(id, parseInt(speed)); - showToast(`风扇 ${id} 速度已设置为 ${speed}%`, 'success'); - } catch (e) { showToast('设置风扇失败: ' + e.message, 'error'); } + showToast(typeof t === 'function' ? t('fan.speedSet', { id, speed }) : `风扇 ${id} 速度已设置为 ${speed}%`, 'success'); + } catch (e) { showToast((typeof t === 'function' ? t('fan.setFanFailed', { msg: e.message }) : '设置风扇失败: ' + e.message), 'error'); } } async function setFanMode(id, mode) { try { await api.call('fan.mode', { id: id, mode: mode }); - showToast(`风扇 ${id} 模式已切换为 ${mode}`, 'success'); + showToast(typeof t === 'function' ? t('fan.modeSwitch', { id, mode }) : `风扇 ${id} 模式已切换为 ${mode}`, 'success'); await refreshFans(); - } catch (e) { showToast('设置风扇模式失败: ' + e.message, 'error'); } + } catch (e) { showToast((typeof t === 'function' ? t('fan.setFanModeFailed', { msg: e.message }) : '设置风扇模式失败: ' + e.message), 'error'); } } async function refreshFans() { @@ -2061,7 +2063,7 @@ async function loadVariableBindStatus() { if (priorityVars.length > 0) { const group1 = document.createElement('optgroup'); - group1.label = '温度变量'; + group1.label = typeof t === 'function' ? t('dataWidget.tempVariables') : '温度变量'; priorityVars.forEach(v => { const opt = document.createElement('option'); opt.value = v.name; @@ -2073,7 +2075,7 @@ async function loadVariableBindStatus() { if (otherVars.length > 0) { const group2 = document.createElement('optgroup'); - group2.label = '其他数值变量'; + group2.label = typeof t === 'function' ? t('dataWidget.otherNumericVariables') : '其他数值变量'; otherVars.forEach(v => { const opt = document.createElement('option'); opt.value = v.name; @@ -2164,7 +2166,7 @@ async function unbindTempVariable() { * 保存 AGX 服务器配置 (保留用于兼容) */ async function saveAgxConfig() { - showToast('AGX 配置已移至变量绑定', 'info'); + showToast((typeof t === 'function' ? t('toast.agxMovedToBinding') : 'AGX 配置已移至变量绑定'), 'info'); await loadVariableBindStatus(); } @@ -2412,10 +2414,10 @@ async function serviceAction(name, action) { if (action === 'restart') await api.serviceRestart(name); else if (action === 'start') await api.serviceStart(name); else if (action === 'stop') await api.serviceStop(name); - showToast(`服务 ${name} ${action} 成功`, 'success'); + showToast(typeof t === 'function' ? t('toast.serviceSuccess', { name, action }) : `服务 ${name} ${action} 成功`, 'success'); await refreshSystemPage(); } catch (e) { - showToast(`操作失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.operationFailedMsg', { msg: e.message }) : `操作失败: ${e.message}`, 'error'); } } @@ -2478,7 +2480,7 @@ async function refreshSystemLeds() { } } catch (e) { console.error('LED list error:', e); - container.innerHTML = `
加载失败: ${e.message}
`; + container.innerHTML = `
${typeof t === 'function' ? t('common.loadFailedMsg', { msg: e.message }) : '加载失败: ' + e.message}
`; } } @@ -2597,6 +2599,9 @@ let dataWidgetsIntervalId = null; // 标记是否正在保存(防止重复保存) let dataWidgetsSaving = false; +// 标记是否正在刷新(防止重叠刷新) +let dataWidgetsRefreshing = false; + /** * 加载数据组件配置 * 优先级:后端 API (SD卡/NVS) > localStorage (兼容旧版) @@ -2964,23 +2969,23 @@ function renderWidgetHtml(widget) {
- - - ${isReading ? '读取中...' : '已停止'} + ${isReading ? (typeof t === 'function' ? t('common.reading') : '读取中...') : (typeof t === 'function' ? t('status.stopped') : '已停止')}
-
点击「读取」开始获取日志
+
${typeof t === 'function' ? t('dataWidget.clickToRead') : '点击「读取」开始获取日志'}
`; break; @@ -3116,10 +3121,10 @@ function updateWidgetValue(widget, value) { const th = thresholds || [0, 50, 80]; const cl = colors || ['#40c057', '#fab005', '#fa5252']; let statusColor = cl[0]; - let statusText = '正常'; + let statusText = typeof t === 'function' ? t('dataWidget.statusNormal') : '正常'; if (!isNaN(numVal)) { - if (numVal >= th[2]) { statusColor = cl[2]; statusText = '警告'; } - else if (numVal >= th[1]) { statusColor = cl[1]; statusText = '注意'; } + if (numVal >= th[2]) { statusColor = cl[2]; statusText = typeof t === 'function' ? t('dataWidget.statusWarning') : '警告'; } + else if (numVal >= th[1]) { statusColor = cl[1]; statusText = typeof t === 'function' ? t('dataWidget.statusAttention') : '注意'; } } if (lightEl) lightEl.style.background = statusColor; if (valueEl) valueEl.textContent = statusText; @@ -3170,55 +3175,63 @@ async function initDataWidgets() { /** * 刷新所有组件的数据 + * 使用 automation.variables.list 一次获取全部变量,避免 N 次 get 串行调用 */ async function refreshDataWidgets() { - // 先批量获取所有需要的变量 - const varNames = new Set(); - dataWidgets.forEach(w => { - if (w.type !== 'log' && w.expression) { - // 从表达式中提取变量名 - const matches = w.expression.match(/\$\{([^}]+)\}/g); - if (matches) { - matches.forEach(m => varNames.add(m.slice(2, -1).trim())); + if (dataWidgetsRefreshing) return; + dataWidgetsRefreshing = true; + + try { + // 先收集所有需要的变量名 + const varNames = new Set(); + dataWidgets.forEach(w => { + if (w.type !== 'log' && w.expression) { + const matches = w.expression.match(/\$\{([^}]+)\}/g); + if (matches) { + matches.forEach(m => varNames.add(m.slice(2, -1).trim())); + } } - } - if (w.expression2) { - const matches = w.expression2.match(/\$\{([^}]+)\}/g); - if (matches) { - matches.forEach(m => varNames.add(m.slice(2, -1).trim())); + if (w.expression2) { + const matches = w.expression2.match(/\$\{([^}]+)\}/g); + if (matches) { + matches.forEach(m => varNames.add(m.slice(2, -1).trim())); + } } - } - }); - - // 获取所有变量的值 - const variables = {}; - for (const name of varNames) { - try { - const resp = await api.call('automation.variables.get', { name }); - if (resp.code === 0 && resp.data) { - variables[name] = resp.data.value; + }); + + // 一次 API 调用获取全部变量(替代 N 次 variables.get) + const variables = {}; + if (varNames.size > 0) { + try { + const resp = await api.call('automation.variables.list'); + if (resp.code === 0 && resp.data?.variables) { + for (const v of resp.data.variables) { + if (varNames.has(v.name) && v.value !== undefined) { + variables[v.name] = v.value; + } + } + } + } catch (e) { + console.warn('获取变量列表失败:', e); } - } catch (e) { - console.warn('获取变量失败:', name, e); } - } - - // 更新每个组件(日志组件不自动刷新) - for (const widget of dataWidgets) { - if (widget.type === 'log') { - // 日志组件由用户手动控制,不自动刷新 - continue; - } else if (widget.expression) { - const value = evaluateExpression(widget.expression, variables); - updateWidgetValue(widget, value); - - // 处理副值 - if (widget.expression2) { - widget.subValue = evaluateExpression(widget.expression2, variables); + + // 更新每个组件(日志组件不自动刷新) + for (const widget of dataWidgets) { + if (widget.type === 'log') { + continue; + } else if (widget.expression) { + const value = evaluateExpression(widget.expression, variables); + updateWidgetValue(widget, value); + if (widget.expression2) { + widget.subValue = evaluateExpression(widget.expression2, variables); + } + } else { + updateWidgetValue(widget, null); } - } else { - updateWidgetValue(widget, null); } + } finally { + dataWidgetsRefreshing = false; } } @@ -3259,7 +3272,7 @@ function toggleLogCollapse(widgetId) { if (btn) { btn.innerHTML = ``; - btn.title = widget._isCollapsed ? '展开日志' : '折叠日志'; + btn.title = widget._isCollapsed ? (typeof t === 'function' ? t('dataWidget.expandLog') : '展开日志') : (typeof t === 'function' ? t('dataWidget.collapseLog') : '折叠日志'); } // 保存状态 @@ -3288,7 +3301,7 @@ function toggleLogReading(widgetId) { function startLogReading(widgetId) { const widget = dataWidgets.find(w => w.id === widgetId); if (!widget || !widget.expression) { - showToast('请先配置日志变量', 'warning'); + showToast((typeof t === 'function' ? t('dataWidget.configLogVariableFirst') : '请先配置日志变量'), 'warning'); return; } @@ -3304,7 +3317,7 @@ function startLogReading(widgetId) { if (toolbar) toolbar.classList.remove('dw-log-toolbar-collapsed'); if (btn) { btn.innerHTML = ''; - btn.title = '折叠日志'; + btn.title = typeof t === 'function' ? t('dataWidget.collapseLog') : '折叠日志'; } } @@ -3346,10 +3359,10 @@ function updateLogToggleButton(widgetId, isReading) { if (btn) { btn.className = `btn btn-sm ${isReading ? 'btn-danger' : 'btn-service-style'}`; - btn.innerHTML = ` ${isReading ? '停止' : '读取'}`; + btn.innerHTML = ` ${isReading ? (typeof t === 'function' ? t('fanPage.stopReading') : '停止') : (typeof t === 'function' ? t('fanPage.reading') : '读取')}`; } if (status) { - status.textContent = isReading ? '读取中...' : '已停止'; + status.textContent = isReading ? (typeof t === 'function' ? t('common.reading') : '读取中...') : (typeof t === 'function' ? t('status.stopped') : '已停止'); } } @@ -3364,7 +3377,7 @@ async function refreshLogOnce(widgetId) { if (!container) return; if (!widget.expression) { - container.innerHTML = '
未配置日志变量
'; + container.innerHTML = '
' + (typeof t === 'function' ? t('fanPage.noLogVariable') : '未配置日志变量') + '
'; return; } @@ -3372,7 +3385,7 @@ async function refreshLogOnce(widgetId) { // 从表达式中提取变量名 const varMatch = widget.expression.match(/\$\{([^}]+)\}/); if (!varMatch) { - container.innerHTML = '
无效的变量表达式
'; + container.innerHTML = '
' + (typeof t === 'function' ? t('fanPage.invalidExpression') : '无效的变量表达式') + '
'; return; } @@ -3380,7 +3393,7 @@ async function refreshLogOnce(widgetId) { const result = await api.call('automation.variables.get', { name: varName }); if (result.code !== 0 || result.data?.value === undefined) { - container.innerHTML = '
变量不存在或无数据
'; + container.innerHTML = '
' + (typeof t === 'function' ? t('fanPage.variableNoData') : '变量不存在或无数据') + '
'; return; } @@ -3389,7 +3402,7 @@ async function refreshLogOnce(widgetId) { } catch (e) { console.warn('获取日志变量失败:', e); - container.innerHTML = '
读取失败
'; + container.innerHTML = '
' + (typeof t === 'function' ? t('fanPage.readFailed') : '读取失败') + '
'; } } @@ -3425,7 +3438,7 @@ function appendLogToWidget(widgetId, newText, maxLines) { // 渲染 if (existingLines.length === 0) { - container.innerHTML = '
暂无日志
'; + container.innerHTML = '
' + (typeof t === 'function' ? t('fanPage.noLogs') : '暂无日志') + '
'; } else { container.innerHTML = existingLines.map(line => { const escaped = escapeHtml(line); @@ -3450,7 +3463,7 @@ function appendLogToWidget(widgetId, newText, maxLines) { function clearLogWidget(widgetId) { const container = document.getElementById(`dw-${widgetId}-log`); if (container) { - container.innerHTML = '
已清空
'; + container.innerHTML = '
' + (typeof t === 'function' ? t('fanPage.cleared') : '已清空') + '
'; } } @@ -3682,7 +3695,7 @@ function createNewWidget(type) { widget.maxLines = defaults.maxLines || 15; widget.refreshInterval = 2000; widget.layout = 'full'; // 日志组件默认独占一行 - widget.label = '日志流'; + widget.label = typeof t === 'function' ? t('dataWidget.presetLog') : '日志流'; } dataWidgets.push(widget); @@ -3930,7 +3943,7 @@ function deleteDataWidget(widgetId) { const widget = dataWidgets[idx]; - if (!confirm(`确定要删除"${widget.label}"组件吗?`)) return; + if (!confirm(typeof t === 'function' ? t('ui.confirmDeleteWidget', { label: widget.label }) : `确定要删除"${widget.label}"组件吗?`)) return; dataWidgets.splice(idx, 1); saveDataWidgets(); @@ -4017,7 +4030,7 @@ async function refreshQuickActions() { } const statusIcon = isRunning ? '' : ''; - const statusTitle = isRunning ? '进程运行中' : '进程未运行'; + const statusTitle = isRunning ? (typeof t === 'function' ? t('automationPage.processRunning') : '进程运行中') : (typeof t === 'function' ? t('automationPage.processNotRunning') : '进程未运行'); let serviceStatusHtml = ''; if (nohupInfo.serviceMode && nohupInfo.varName && isRunning) { serviceStatusHtml = ` @@ -4026,22 +4039,27 @@ async function refreshQuickActions() { `; } + const viewLogText = typeof t === 'function' ? t('automationPage.logTitle') : '日志'; + const stopText = typeof t === 'function' ? t('automationPage.stopProcess') : '停止'; + const viewLogTitle = typeof t === 'function' ? t('automationPage.viewLog') : '查看日志'; + const stopTitle = typeof t === 'function' ? t('ssh.stopProcess') : '终止进程'; nohupBtns = ` ${statusIcon} ${serviceStatusHtml}
- -
`; } + const processRunningMsg = typeof t === 'function' ? t('toast.processRunning') : '进程正在运行中,请先停止'; const cardOnClick = (nohupInfo && isRunning) - ? `showToast('进程正在运行中,请先停止', 'warning')` + ? `showToast(${JSON.stringify(processRunningMsg)}, 'warning')` : `triggerQuickAction('${escapeHtml(rule.id)}')`; const cleanName = rule.name.replace(/^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F1E0}-\u{1F1FF}\u200D]+\s*/gu, '').trim(); cardsHtml.push(` @@ -4124,7 +4142,7 @@ async function updateQuickActionServiceStatus() { // 如果进程未运行,始终显示"未启动" if (!isRunning) { - valueEl.textContent = '未启动'; + valueEl.textContent = (typeof t === 'function' ? t('sshPage.statusIdle') : '未启动'); container.className = 'quick-action-service-status status-idle'; continue; } @@ -4136,11 +4154,11 @@ async function updateQuickActionServiceStatus() { valueEl.textContent = getServiceStatusLabel(status); container.className = `quick-action-service-status status-${status}`; } else { - valueEl.textContent = '检测中'; + valueEl.textContent = (typeof t === 'function' ? t('sshPage.statusChecking') : '检测中'); container.className = 'quick-action-service-status status-checking'; } } catch (e) { - valueEl.textContent = '未知'; + valueEl.textContent = (typeof t === 'function' ? t('sshPage.statusUnknown') : '未知'); container.className = 'quick-action-service-status status-unknown'; } } @@ -4158,7 +4176,7 @@ async function triggerQuickAction(ruleId) { const card = event?.currentTarget || document.getElementById(`quick-action-${ruleId}`); if (!card) { console.error('triggerQuickAction: card not found for ruleId=', ruleId); - showToast('无法找到操作卡片', 'error'); + showToast((typeof t === 'function' ? t('toast.cardNotFound') : '无法找到操作卡片'), 'error'); return; } @@ -4169,7 +4187,7 @@ async function triggerQuickAction(ruleId) { const now = Date.now(); if (now < _quickActionTriggerCooldownUntil && ruleId !== _quickActionLastTriggeredId) { - showToast('请等待几秒后再触发其他模型', 'warning'); + showToast((typeof t === 'function' ? t('toast.waitBeforeTrigger') : '请等待几秒后再触发其他模型'), 'warning'); return; } @@ -4188,14 +4206,14 @@ async function triggerQuickAction(ruleId) { const result = await api.call('automation.rules.trigger', { id: ruleId }); if (result.code === 0) { - showToast('操作已执行', 'success'); + showToast((typeof t === 'function' ? t('toast.operationExecuted') : '操作已执行'), 'success'); _quickActionLastTriggeredId = ruleId; _quickActionTriggerCooldownUntil = Date.now() + 5000; // 5 秒内勿触发其他规则,避免后端串行导致第二个未执行 card.classList.add('is-running'); card.style.pointerEvents = ''; setTimeout(() => refreshQuickActions(), 2500); } else { - showToast((result.message || '执行失败'), 'error'); + showToast((result.message || (typeof t === 'function' ? t('toast.execFailed') : '执行失败')), 'error'); card.style.pointerEvents = ''; // 失败时恢复点击 // 恢复原始图标 if (iconEl && originalIcon) { @@ -4206,7 +4224,7 @@ async function triggerQuickAction(ruleId) { card.classList.remove('triggering'); } catch (e) { console.error('triggerQuickAction error:', e); - showToast('执行失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.execFailedMsg', { msg: e.message }) : '执行失败: ' + e.message), 'error'); if (card) { card.classList.remove('triggering'); card.style.pointerEvents = ''; @@ -4321,16 +4339,26 @@ async function quickActionViewLog(logFile, hostId) { // 获取主机信息 const host = window._sshHostsData?.[hostId]; if (!host) { - showToast('主机不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.hostNotExistShort') : '主机不存在'), 'error'); return; } - // 显示日志模态框 + const logTitle = typeof t === 'function' ? t('automationPage.logTitle') : '日志'; + const stopTrackingText = typeof t === 'function' ? t('automationPage.stopTracking') : '停止跟踪'; + const intervalText = typeof t === 'function' ? t('automationPage.interval') : '间隔'; + const interval1Sec = typeof t === 'function' ? t('automationPage.interval1Sec') : '1秒'; + const interval2Sec = typeof t === 'function' ? t('automationPage.interval2Sec') : '2秒'; + const interval3Sec = typeof t === 'function' ? t('automationPage.interval3Sec') : '3秒'; + const interval5Sec = typeof t === 'function' ? t('automationPage.interval5Sec') : '5秒'; + const interval10Sec = typeof t === 'function' ? t('automationPage.interval10Sec') : '10秒'; + const interval30Sec = typeof t === 'function' ? t('automationPage.interval30Sec') : '30秒'; + const realTimeText = typeof t === 'function' ? t('automationPage.realTimeUpdating') : '● 实时更新中'; + const closeText = typeof t === 'function' ? t('common.close') : '关闭'; const modalHtml = ` @@ -4383,21 +4411,24 @@ async function quickActionRefreshLog(logFile, hostId) { port: host.port, user: host.username, keyid: host.keyid, - command: `if [ -f ${logFile} ]; then cat ${logFile}; else echo '[日志文件不存在或为空]'; fi`, + command: `if [ -f ${logFile} ]; then cat ${logFile}; else echo '${typeof t === 'function' ? t('automationPage.logFileEmptyBracket') : '[日志文件不存在或为空]'}'; fi`, timeout_ms: 15000 }); if (result.code !== 0 || !result.data) { - contentEl.textContent = '[获取失败] ' + (result.message || 'code=' + result.code); + contentEl.textContent = (typeof t === 'function' ? t('automationPage.logFetchFailed') : '[获取失败]') + ' ' + (result.message || 'code=' + result.code); return; } - const output = (result.data.stdout || result.data.stderr || '').trim() || '[空]'; + const _empty = typeof t === 'function' ? t('automationPage.logEmptyBracket') : '[空]'; + const output = (result.data.stdout || result.data.stderr || '').trim() || _empty; if (output !== quickActionLastContent) { contentEl.textContent = output; contentEl.scrollTop = contentEl.scrollHeight; quickActionLastContent = output; } } catch (e) { - contentEl.textContent = '[错误] ' + e.message + '\n\n若设备繁忙可稍后重试。'; + const _err = typeof t === 'function' ? t('automationPage.logError') : '[错误]'; + const _retry = typeof t === 'function' ? t('automationPage.deviceBusyRetry') : '若设备繁忙可稍后重试。'; + contentEl.textContent = _err + ' ' + e.message + '\n\n' + _retry; } } @@ -4422,11 +4453,15 @@ function startQuickLogTail(logFile, hostId, intervalMs = 5000) { const status = document.getElementById('quick-log-status'); if (btn) { - btn.innerHTML = ' 停止跟踪'; + const stopText = typeof t === 'function' ? t('automationPage.stopTracking') : '停止跟踪'; + btn.innerHTML = ' ' + stopText; btn.classList.remove('btn-service-style'); btn.classList.add('btn-danger'); } - if (status) status.innerHTML = '● 实时更新中'; + if (status) { + const realTimeText = typeof t === 'function' ? t('automationPage.realTimeUpdating') : '● 实时更新中'; + status.innerHTML = '' + realTimeText + ''; + } quickActionLastContent = ''; // 定义刷新函数 @@ -4479,11 +4514,12 @@ function stopQuickLogTail() { const status = document.getElementById('quick-log-status'); if (btn) { - btn.innerHTML = ' 开始跟踪'; + const startText = typeof t === 'function' ? t('automationPage.startTracking') : '开始跟踪'; + btn.innerHTML = ' ' + startText; btn.classList.remove('btn-danger'); btn.classList.add('btn-service-style'); } - if (status) status.textContent = '已暂停'; + if (status) status.textContent = (typeof t === 'function' ? t('automationPage.trackingStopped') : '已暂停'); } /** @@ -4516,16 +4552,17 @@ function closeQuickLogModal() { async function quickActionStopProcess(pidFile, hostId, cmdName, varName) { const host = window._sshHostsData?.[hostId]; if (!host) { - showToast('主机不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.hostNotExistShort') : '主机不存在'), 'error'); return; } - if (!confirm(`确定要终止 "${cmdName}" 吗?`)) { + const confirmMsg = typeof t === 'function' ? t('toast.confirmTerminateCmd', { cmdName }) : `确定要终止 "${cmdName}" 吗?`; + if (!confirm(confirmMsg)) { return; } try { - showToast('正在终止进程...', 'info'); + showToast((typeof t === 'function' ? t('toast.terminatingProcess') : '正在终止进程...'), 'info'); // 终止进程:支持 PID 和 PID-1 双重检测 // 原因:nohup cmd & 的 $! 可能记录了中间 shell PID,真实主进程 PID = $! - 1 @@ -4567,13 +4604,13 @@ async function quickActionStopProcess(pidFile, hostId, cmdName, varName) { if (result.code === 0 && result.data) { const output = (result.data.stdout || '').trim(); const msgMap = { - 'TERMINATED': '服务已停止', - 'FORCE_KILLED': '服务已强制终止', - 'ALREADY_STOPPED': '进程已不在运行,已清理 PID 文件', - 'NO_PID_FILE': 'PID 文件不存在', - 'STILL_RUNNING': '无法终止进程,请手动处理' + 'TERMINATED': typeof t === 'function' ? t('toast.terminateTERMINATED') : '服务已停止', + 'FORCE_KILLED': typeof t === 'function' ? t('toast.terminateFORCE_KILLED') : '服务已强制终止', + 'ALREADY_STOPPED': typeof t === 'function' ? t('toast.terminateALREADY_STOPPED') : '进程已不在运行,已清理 PID 文件', + 'NO_PID_FILE': typeof t === 'function' ? t('toast.terminateNO_PID_FILE') : 'PID 文件不存在', + 'STILL_RUNNING': typeof t === 'function' ? t('toast.terminateSTILL_RUNNING') : '无法终止进程,请手动处理' }; - const msg = msgMap[output] || (output || '操作完成'); + const msg = msgMap[output] || (output || (typeof t === 'function' ? t('toast.terminateDefault') : '操作完成')); const isSuccess = ['TERMINATED', 'FORCE_KILLED', 'ALREADY_STOPPED'].includes(output); showToast(msg, isSuccess ? 'success' : (output === 'STILL_RUNNING' ? 'error' : 'info')); // 停止成功后,清除服务模式状态变量(否则"就绪"标签会残留) @@ -4587,10 +4624,10 @@ async function quickActionStopProcess(pidFile, hostId, cmdName, varName) { // 刷新状态 setTimeout(() => refreshQuickActions(), 1500); } else { - showToast((result.message || '操作失败'), 'error'); + showToast((result.message || (typeof t === 'function' ? t('common.operationFailed') : '操作失败')), 'error'); } } catch (e) { - showToast('错误: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.errorMsg', { msg: e.message }) : '错误: ' + e.message), 'error'); } } @@ -4718,7 +4755,7 @@ async function loadLedPage() {
- +
@@ -4775,7 +4812,7 @@ async function refreshLedPage() { } } catch (e) { console.error('LED list error:', e); - container.innerHTML = `
加载失败: ${e.message}
`; + container.innerHTML = `
${typeof t === 'function' ? t('common.loadFailedMsg', { msg: e.message }) : '加载失败: ' + e.message}
`; } } @@ -4911,9 +4948,9 @@ async function fillColorFromPicker(device, color) { await api.ledFill(device, color); ledStates[device] = true; updateLedCardState(device, true); - showToast(`${device} 已填充 ${color}`, 'success'); + showToast(typeof t === 'function' ? t('toast.ledFilled', { device, color }) : `${device} 已填充 ${color}`, 'success'); } catch (e) { - showToast(`填充失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.ledFillFailed') + ': ' + e.message : `填充失败: ${e.message}`, 'error'); } } @@ -4924,9 +4961,9 @@ async function quickFillColor(device, color) { await api.ledFill(device, color); ledStates[device] = true; updateLedCardState(device, true, null); - showToast(`${device} → ${color}`, 'success'); + showToast(typeof t === 'function' ? t('toast.ledFilled', { device, color }) : `${device} → ${color}`, 'success'); } catch (e) { - showToast(`填充失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.ledFillFailed') + ': ' + e.message : `填充失败: ${e.message}`, 'error'); } } @@ -4936,9 +4973,9 @@ async function quickStartEffect(device, effect) { selectedEffects[device] = effect; ledStates[device] = true; updateLedCardState(device, true, effect); - showToast(`${device}: ${effect}`, 'success'); + showToast(typeof t === 'function' ? t('toast.ledEffectStarted', { device, effect }) : `${device}: ${effect}`, 'success'); } catch (e) { - showToast(`启动失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.ledEffectStartFailed') + ': ' + e.message : `启动失败: ${e.message}`, 'error'); } } @@ -4953,7 +4990,7 @@ async function allLedsOff() { console.error(`关闭 ${dev.name} 失败:`, e); } } - showToast('全部 LED 已关闭', 'success'); + showToast((typeof t === 'function' ? t('toast.allLedsOff') : '全部 LED 已关闭'), 'success'); } function updateLedCardState(device, isOn, effect = undefined) { @@ -4971,13 +5008,13 @@ function updateLedCardState(device, isOn, effect = undefined) { const statusEl = card.querySelector('.led-device-status'); if (statusEl) { if (!isOn) { - statusEl.textContent = '已关闭'; + statusEl.textContent = typeof t === 'function' ? t('ledPage.statusOff') : '已关闭'; statusEl.className = 'led-device-status off'; } else if (effect) { statusEl.textContent = `▶ ${effectDisplayName(effect)}`; statusEl.className = 'led-device-status effect'; } else { - statusEl.textContent = '常亮'; + statusEl.textContent = typeof t === 'function' ? t('ledPage.statusOn') : '常亮'; statusEl.className = 'led-device-status on'; } } @@ -5026,15 +5063,15 @@ function openColorModal(device) { const title = document.getElementById('led-modal-title'); const body = document.getElementById('led-modal-body'); - title.textContent = `${device} - 颜色设置`; + title.textContent = `${device} - ${typeof t === 'function' ? t('ledPage.colorSettings') : '颜色设置'}`; body.innerHTML = ` `; return; } @@ -6365,7 +6402,7 @@ async function loadFilePickerDirectory(path) { }); if (filtered.length === 0) { - listContainer.innerHTML = '
无图片文件
'; + listContainer.innerHTML = '
' + (typeof t === 'function' ? t('filePage.noImages') : '无图片文件') + '
'; return; } @@ -6404,7 +6441,7 @@ async function createAndOpenDir(path) { await api.storageMkdir(path); await loadFilePickerDirectory(path); } catch (e) { - showToast('创建目录失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.createDirFailed', { msg: e.message }) : '创建目录失败: ' + e.message), 'error'); } } @@ -6467,15 +6504,15 @@ async function displayImage() { const path = pathInput.value.trim(); if (!path) { - showToast('请输入图像路径', 'error'); + showToast((typeof t === 'function' ? t('toast.enterImagePath') : '请输入图像路径'), 'error'); return; } try { const result = await api.ledImage(path, 'matrix', centerCheckbox.checked); - showToast(`图像显示成功`, 'success'); + showToast(typeof t === 'function' ? t('toast.imageDisplayed') : '图像显示成功', 'success'); } catch (e) { - showToast(`显示图像失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.ledImageFailed') + ': ' + e.message : `显示图像失败: ${e.message}`, 'error'); } } @@ -6488,7 +6525,7 @@ async function generateQrCode() { const text = textInput.value.trim(); if (!text) { - showToast('请输入 QR 码内容', 'error'); + showToast((typeof t === 'function' ? t('toast.enterQrContent') : '请输入 QR 码内容'), 'error'); return; } @@ -6505,9 +6542,9 @@ async function generateQrCode() { try { const result = await api.ledQrcode(text, params); - showToast(`QR 码生成成功`, 'success'); + showToast(typeof t === 'function' ? t('toast.qrGenerated') : 'QR 码生成成功', 'success'); } catch (e) { - showToast(`生成 QR 码失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.ledQrFailed') + ': ' + e.message : `生成 QR 码失败: ${e.message}`, 'error'); } } @@ -6541,8 +6578,8 @@ async function loadFontList() { if (fonts.length === 0) { // 没有字体时添加占位选项 - fontSelect.innerHTML = ''; - showToast('未找到字体文件,请上传到 /sdcard/fonts', 'info'); + fontSelect.innerHTML = ''; + showToast((typeof t === 'function' ? t('toast.fontNotFound') : '未找到字体文件,请上传到 /sdcard/fonts'), 'info'); } else { fonts.forEach(f => { const option = document.createElement('option'); @@ -6580,7 +6617,7 @@ async function displayText() { const text = textInput.value.trim(); if (!text) { - showToast('请输入显示文本', 'error'); + showToast((typeof t === 'function' ? t('toast.enterDisplayText') : '请输入显示文本'), 'error'); return; } @@ -6602,9 +6639,9 @@ async function displayText() { try { const result = await api.ledText(text, params); - showToast(`文本显示成功`, 'success'); + showToast(typeof t === 'function' ? t('toast.textDisplayed') : '文本显示成功', 'success'); } catch (e) { - showToast(`显示文本失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.ledTextFailed') + ': ' + e.message : `显示文本失败: ${e.message}`, 'error'); } } @@ -6612,9 +6649,9 @@ async function displayText() { async function stopText() { try { await api.ledTextStop('matrix'); - showToast('文本已停止', 'success'); + showToast((typeof t === 'function' ? t('toast.textStopped') : '文本已停止'), 'success'); } catch (e) { - showToast(`停止失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.ledTextStopFailed') + ': ' + e.message : `停止失败: ${e.message}`, 'error'); } } @@ -6639,7 +6676,7 @@ const filterConfig = { 'sepia': { params: [], defaults: {} } }; -// 参数标签和范围定义 +// 参数标签和范围定义(label 为中文兜底,显示时用 getParamLabel 做 i18n) const paramLabels = { 'speed': { label: '速度', min: 1, max: 100, unit: '', help: '闪耀效果:推荐1-10,低值更慢' }, 'intensity': { label: '强度', min: 0, max: 255, unit: '', help: '亮度增益倍数,推荐100-200产生明显对比' }, @@ -6659,6 +6696,12 @@ const paramLabels = { let selectedFilter = null; +function getParamLabel(paramKey) { + const keyMap = { speed: 'ledPage.paramSpeed', intensity: 'ledPage.paramIntensity', wavelength: 'ledPage.paramWavelength', amplitude: 'ledPage.paramAmplitude', direction: 'ledPage.paramDirection', angle: 'ledPage.paramAngle', width: 'ledPage.paramWidth', frequency: 'ledPage.paramFrequency', saturation: 'ledPage.paramSaturation', density: 'ledPage.paramDensity', decay: 'ledPage.paramDecay', scale: 'ledPage.paramScale', levels: 'ledPage.paramLevels', amount: 'ledPage.paramAmount' }; + const key = keyMap[paramKey]; + return (typeof t === 'function' && key) ? t(key) : (paramLabels[paramKey]?.label || paramKey); +} + // 选择滤镜 function selectFilter(filterName, btnElement) { selectedFilter = filterName; @@ -6669,7 +6712,7 @@ function selectFilter(filterName, btnElement) { // 更新显示的滤镜名称 const nameSpan = document.getElementById('selected-filter-name'); - if (nameSpan) nameSpan.textContent = `已选择: ${filterName}`; + if (nameSpan) nameSpan.textContent = typeof t === 'function' ? t('led.selectedFilter', { filter: filterName }) : `已选择: ${filterName}`; // 启用应用按钮 const applyBtn = document.getElementById('apply-filter-btn'); @@ -6692,7 +6735,7 @@ function selectFilter(filterName, btnElement) { row.style.cssText = 'display: flex; align-items: center; gap: 10px; margin-bottom: 10px;'; const label = document.createElement('label'); - label.textContent = paramInfo.label; + label.textContent = getParamLabel(param); label.style.minWidth = '60px'; const slider = document.createElement('input'); @@ -6727,7 +6770,7 @@ function selectFilter(filterName, btnElement) { // 应用选中的滤镜 async function applySelectedFilter() { if (!selectedFilter) { - showToast('请先选择滤镜', 'error'); + showToast((typeof t === 'function' ? t('toast.selectFilter') : '请先选择滤镜'), 'error'); return; } @@ -6755,9 +6798,9 @@ async function applySelectedFilter() { try { await api.call('led.filter.start', params); - showToast(`已应用滤镜: ${selectedFilter}`, 'success'); + showToast(typeof t === 'function' ? t('toast.filterApplied', { filter: selectedFilter }) : `已应用滤镜: ${selectedFilter}`, 'success'); } catch (e) { - showToast(`应用滤镜失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.filterApplyFailed') + ': ' + e.message : `应用滤镜失败: ${e.message}`, 'error'); } } @@ -6771,7 +6814,7 @@ async function applyFilter(filterName, btnElement) { async function stopFilter() { try { await api.ledFilterStop('matrix'); - showToast('滤镜已停止', 'success'); + showToast((typeof t === 'function' ? t('toast.filterStopped') : '滤镜已停止'), 'success'); // 移除滤镜按钮高亮和选中状态 document.querySelectorAll('.filter-btn').forEach(btn => { @@ -6782,13 +6825,13 @@ async function stopFilter() { // 重置 UI const nameSpan = document.getElementById('selected-filter-name'); - if (nameSpan) nameSpan.textContent = '未选择滤镜'; + if (nameSpan) nameSpan.textContent = typeof t === 'function' ? t('ledPage.noFilterSelected') : '未选择滤镜'; const applyBtn = document.getElementById('apply-filter-btn'); if (applyBtn) applyBtn.disabled = true; const paramsDiv = document.getElementById('filter-params'); if (paramsDiv) paramsDiv.style.display = 'none'; } catch (e) { - showToast(`停止滤镜失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.filterStopFailed') + ': ' + e.message : `停止滤镜失败: ${e.message}`, 'error'); } } @@ -7011,7 +7054,7 @@ async function loadNetworkPage() {
- - + +
- +
-
- +
+ ${typeof t === 'function' ? t('dataWidget.icon') : '图标'}
@@ -8664,7 +8707,7 @@ async function loadCommandsPage() {
${typeof t === 'function' ? t('ssh.stopOnMatchHint') : '适用于 ping 等持续运行的命令,匹配成功后自动终止'}
@@ -9020,7 +9063,7 @@ function addCommandsPageStyles() { font-size: 1.2em; border: 2px solid var(--border); border-radius: var(--radius-sm); - background: var(--bg-muted); + background: transparent; cursor: pointer; } .icon-btn:hover, .icon-btn.selected { @@ -9044,6 +9087,31 @@ function addCommandsPageStyles() { #command-modal .form-group textarea { min-height: 150px; } + /* 新建指令模态框 - 图标区块与添加规则一致,带底色包裹 */ + #command-modal .config-section.config-section-icon { + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px; + margin-bottom: 16px; + background: var(--bg-muted); + } + #command-modal .config-section-icon .config-title { + font-weight: 600; + font-size: 0.95rem; + margin-bottom: 12px; + display: block; + } + #command-modal .config-section-icon .icon-type-tabs { + margin-bottom: 10px; + } + /* 图标网格去白底:icon-picker 和 icon-btn 透明,与 config-section 灰底融为一体 */ + #command-modal .config-section-icon .icon-picker { + background: transparent; + padding: 0; + } + #command-modal .config-section-icon .icon-btn { + background: transparent; + } `; document.head.appendChild(style); } @@ -9061,8 +9129,8 @@ async function loadHostSelector() { if (hosts.length === 0) { container.innerHTML = `
-

暂无已部署主机

-

请先到 安全 页面部署 SSH 公钥

+

${typeof t === 'function' ? t('sshPage.noDeployedHostsMsg') : '暂无已部署主机'}

+

${typeof t === 'function' ? t('sshPage.deployKeyAtSecurityHint') : '请先到 安全 页面部署 SSH 公钥'}

`; return; @@ -9247,12 +9315,12 @@ async function updateServiceStatusInList() { statusEl.textContent = getServiceStatusLabel(status); statusEl.className = `service-status status-${status}`; } else { - statusEl.textContent = '未启动'; + statusEl.textContent = (typeof t === 'function' ? t('sshPage.statusIdle') : '未启动'); statusEl.className = 'service-status status-idle'; } } catch (e) { console.error(`[ServiceStatus] Error getting ${varName}.status:`, e); - statusEl.textContent = '未知'; + statusEl.textContent = (typeof t === 'function' ? t('sshPage.statusUnknown') : '未知'); statusEl.className = 'service-status status-unknown'; } } @@ -9262,15 +9330,13 @@ async function updateServiceStatusInList() { * 获取服务状态显示文本 */ function getServiceStatusLabel(status) { - const labels = { - 'ready': '就绪', - 'checking': '检测中', - 'timeout': '超时', - 'failed': '失败', - 'idle': '未启动', - 'stopped': '已停止' - }; - return labels[status] || status; + if (typeof t !== 'function') { + const labels = { 'ready': '就绪', 'checking': '检测中', 'timeout': '超时', 'failed': '失败', 'idle': '未启动', 'stopped': '已停止' }; + return labels[status] || status; + } + const keyMap = { 'ready': 'sshPage.statusReady', 'checking': 'sshPage.statusChecking', 'timeout': 'sshPage.statusTimeout', 'failed': 'sshPage.statusFailed', 'idle': 'sshPage.statusIdle', 'stopped': 'sshPage.statusStopped' }; + const key = keyMap[status]; + return key ? t(key) : status; } function showAddCommandModal() { @@ -9355,7 +9421,7 @@ async function showCommandVariables(varName) { // 更新标题 const header = modal.querySelector('.modal-header h2'); - if (header) header.textContent = `指令变量: ${varName}.*`; + if (header) header.textContent = typeof t === 'function' ? t('ui.commandVariablesWithName', { varName }) : `指令变量: ${varName}.*`; body.innerHTML = '
' + t('common.loading') + '
'; modal.classList.remove('hidden'); @@ -9370,7 +9436,7 @@ async function showCommandVariables(varName) { v.source_id === varName || v.name.startsWith(varName + '.')); if (vars.length === 0) { - body.innerHTML = '

该指令暂无变量数据,请先执行一次

'; + body.innerHTML = '

' + (typeof t === 'function' ? t('sshPage.noVariableData') : '该指令暂无变量数据,请先执行一次') + '

'; return; } @@ -9378,10 +9444,10 @@ async function showCommandVariables(varName) { - - - - + + + + @@ -9397,7 +9463,7 @@ async function showCommandVariables(varName) {
变量名类型当前值更新时间${typeof t === 'function' ? t('sshPage.varTableName') : '变量名'}${typeof t === 'function' ? t('sshPage.varTableType') : '类型'}${typeof t === 'function' ? t('sshPage.varTableValue') : '当前值'}${typeof t === 'function' ? t('sshPage.varTableUpdated') : '更新时间'}
`; } else { - body.innerHTML = `

${result.message || '获取变量失败'}

`; + body.innerHTML = `

${result.message || (typeof t === 'function' ? t('sshPage.getVarFailed') : '获取变量失败')}

`; } } catch (error) { body.innerHTML = `

${error.message}

`; @@ -9544,9 +9610,10 @@ async function browseCmdIconImage() { function updateCmdIconPreview(path) { const preview = document.getElementById('cmd-icon-preview'); if (path && path.startsWith('/sdcard/')) { - preview.innerHTML = `icon`; + const loadFailed = typeof t === 'function' ? t('sshPage.iconPreviewFailed') : '加载失败'; + preview.innerHTML = `icon`; } else { - preview.innerHTML = ''; + preview.innerHTML = '' + (typeof t === 'function' ? t('sshPage.iconPreviewNone') : '无') + ''; } } @@ -9635,7 +9702,7 @@ async function saveCommand() { const readyInterval = parseInt(document.getElementById('cmd-ready-interval')?.value) || 5000; if (!name || !command) { - showToast('请填写指令名称和命令', 'warning'); + showToast((typeof t === 'function' ? t('toast.fillCommandNameAndCmd') : '请填写指令名称和命令'), 'warning'); return; } @@ -9653,29 +9720,29 @@ async function saveCommand() { if (cleaned !== command) { document.getElementById('cmd-command').value = cleaned; command = cleaned; - showToast('已自动去除命令中多余的 nohup 包装(后端会自动添加)', 'info'); + showToast((typeof t === 'function' ? t('toast.nohupStripped') : '已自动去除命令中多余的 nohup 包装(后端会自动添加)'), 'info'); } } /* ID 验证(必填) */ if (!cmdId) { - showToast('请填写指令 ID', 'warning'); + showToast((typeof t === 'function' ? t('toast.fillCommandId') : '请填写指令 ID'), 'warning'); document.getElementById('cmd-edit-id').focus(); return; } if (!validateCommandId(document.getElementById('cmd-edit-id'))) { - showToast('指令 ID 格式不正确', 'warning'); + showToast((typeof t === 'function' ? t('toast.commandIdInvalid') : '指令 ID 格式不正确'), 'warning'); document.getElementById('cmd-edit-id').focus(); return; } // 服务模式验证 if (nohup && serviceMode && !readyPattern) { - showToast('启用服务模式时必须设置就绪匹配模式', 'warning'); + showToast((typeof t === 'function' ? t('toast.serviceModeRequiresPattern') : '启用服务模式时必须设置就绪匹配模式'), 'warning'); return; } if (nohup && serviceMode && !varName) { - showToast('启用服务模式时必须设置变量名', 'warning'); + showToast((typeof t === 'function' ? t('toast.serviceModeRequiresVar') : '启用服务模式时必须设置变量名'), 'warning'); return; } @@ -9716,11 +9783,11 @@ async function saveCommand() { if (existingIdx >= 0) { sshCommands[selectedHostId][existingIdx] = cmdData; } - showToast('指令已更新', 'success'); + showToast((typeof t === 'function' ? t('toast.commandUpdated') : '指令已更新'), 'success'); } else { // 新建模式:添加到本地缓存 sshCommands[selectedHostId].push(cmdData); - showToast('指令已创建', 'success'); + showToast((typeof t === 'function' ? t('toast.commandCreated') : '指令已创建'), 'success'); } closeCommandModal(); @@ -9728,7 +9795,7 @@ async function saveCommand() { } catch (e) { console.error('Failed to save command:', e); - showToast('保存指令失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.saveCommandFailedMsg', { msg: e.message }) : '保存指令失败: ' + e.message), 'error'); } } @@ -9736,7 +9803,7 @@ function editCommand(idx) { const cmd = sshCommands[selectedHostId]?.[idx]; if (!cmd) return; - document.getElementById('command-modal-title').textContent = '编辑指令'; + document.getElementById('command-modal-title').textContent = typeof t === 'function' ? t('otaPage.editCommand') : '编辑指令'; /* 编辑模式:设置 ID 并标记为只读 */ const idInput = document.getElementById('cmd-edit-id'); @@ -9839,7 +9906,7 @@ async function exportSshCommand(cmdId) { showExportSshCommandModal(cmdId); } else { // 非开发机:直接使用设备证书加密,询问是否包含主机 - const includeHost = confirm('是否同时导出该指令依赖的主机配置?\n\n点击「确定」将主机配置一起打包(推荐),点击「取消」仅导出指令。'); + const includeHost = confirm(typeof t === 'function' ? t('automation.confirmExportWithHost') : '是否同时导出该指令依赖的主机配置?\n\n点击「确定」将主机配置一起打包(推荐),点击「取消」仅导出指令。'); await doExportSshCommand(cmdId, null, includeHost); } } @@ -10158,7 +10225,7 @@ async function deleteCommand(idx) { const cmd = sshCommands[selectedHostId]?.[idx]; if (!cmd) return; - if (!confirm(`确定要删除指令「${cmd.name}」吗?`)) return; + if (!confirm(typeof t === 'function' ? t('ui.confirmDeleteCmd', { name: cmd.name }) : `确定要删除指令「${cmd.name}」吗?`)) return; try { // 从后端删除(需要指令 ID) @@ -10169,10 +10236,10 @@ async function deleteCommand(idx) { // 从本地缓存删除 sshCommands[selectedHostId].splice(idx, 1); refreshCommandsList(); - showToast('指令已删除', 'success'); + showToast((typeof t === 'function' ? t('toast.commandDeleted') : '指令已删除'), 'success'); } catch (e) { console.error('Failed to delete command:', e); - showToast('删除指令失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.deleteCommandFailedMsg', { msg: e.message }) : '删除指令失败: ' + e.message), 'error'); } } @@ -10189,7 +10256,7 @@ let currentNohupInfo = { /* nohup 快捷操作:查看日志 */ async function nohupViewLog() { if (!currentNohupInfo.logFile || !currentNohupInfo.hostId) { - showToast('没有可用的日志信息', 'warning'); + showToast((typeof t === 'function' ? t('ssh.noLogInfo') : '没有可用的日志信息'), 'warning'); return; } await executeNohupHelperCommand(`cat "${currentNohupInfo.logFile}"`); @@ -10202,7 +10269,7 @@ let lastTailContent = ''; /* nohup 快捷操作:实时跟踪 */ async function nohupTailLog() { if (!currentNohupInfo.logFile || !currentNohupInfo.hostId) { - showToast('没有可用的日志信息', 'warning'); + showToast((typeof t === 'function' ? t('ssh.noLogInfo') : '没有可用的日志信息'), 'warning'); return; } @@ -10220,7 +10287,7 @@ async function nohupTailLog() { tailBtn.style.display = 'none'; stopBtn.style.display = 'inline-block'; - resultPre.textContent += `\n\n━━━━━━━━━━━━━━━━━━━━━━\n开始实时跟踪: ${currentNohupInfo.logFile}\n(点击"停止跟踪"按钮退出)\n━━━━━━━━━━━━━━━━━━━━━━\n`; + resultPre.textContent += `\n\n━━━━━━━━━━━━━━━━━━━━━━\n${typeof t === 'function' ? t('sshPage.startRealTimeTail', { logFile: currentNohupInfo.logFile }) : `开始实时跟踪: ${currentNohupInfo.logFile}\n(点击"停止跟踪"按钮退出)`}\n━━━━━━━━━━━━━━━━━━━━━━\n`; lastTailContent = ''; // 定时获取日志 @@ -10284,7 +10351,7 @@ function nohupStopTail() { /* nohup 快捷操作:检查进程(使用 PID 文件) */ async function nohupCheckProcess() { if (!currentNohupInfo.pidFile || !currentNohupInfo.hostId) { - showToast('没有可用的进程信息', 'warning'); + showToast((typeof t === 'function' ? t('ssh.noProcessInfo') : '没有可用的进程信息'), 'warning'); return; } // 使用 PID 文件检查进程状态,并显示进程详情 @@ -10294,12 +10361,12 @@ async function nohupCheckProcess() { /* nohup 快捷操作:停止进程(使用 PID 文件) */ async function nohupStopProcess() { if (!currentNohupInfo.pidFile || !currentNohupInfo.hostId) { - showToast('没有可用的进程信息', 'warning'); + showToast((typeof t === 'function' ? t('ssh.noProcessInfo') : '没有可用的进程信息'), 'warning'); return; } // 确认对话框 - if (!confirm(`确定要停止此后台进程吗?`)) { + if (!confirm(typeof t === 'function' ? t('ui.confirmStopProcess') : '确定要停止此后台进程吗?')) { return; } @@ -10317,7 +10384,7 @@ async function nohupStopProcess() { async function executeNohupHelperCommand(command) { const host = window._cmdHostsList?.find(h => h.id === currentNohupInfo.hostId); if (!host) { - showToast('主机信息不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.hostNotFound') : '主机信息不存在'), 'error'); return; } @@ -10359,13 +10426,13 @@ async function executeNohupHelperCommand(command) { async function viewServiceLog(idx, safeName) { const cmd = sshCommands[selectedHostId]?.[idx]; if (!cmd) { - showToast('命令不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.cmdNotFound') : '命令不存在'), 'error'); return; } const host = window._cmdHostsList?.find(h => h.id === selectedHostId); if (!host) { - showToast('主机信息不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.hostNotFound') : '主机信息不存在'), 'error'); return; } @@ -10378,7 +10445,7 @@ async function viewServiceLog(idx, safeName) { document.getElementById('cancel-exec-btn').style.display = 'none'; document.getElementById('nohup-actions').style.display = 'none'; - resultPre.textContent = `查看服务日志: ${cmd.name}\n文件: ${logFile}\n\n`; + resultPre.textContent = (typeof t === 'function' ? t('sshPage.viewServiceLogFile', { name: cmd.name, file: logFile }) : `查看服务日志: ${cmd.name}\n文件: ${logFile}`) + '\n\n'; resultSection.scrollIntoView({ behavior: 'smooth' }); try { @@ -10397,12 +10464,12 @@ async function viewServiceLog(idx, safeName) { if (stdout) { resultPre.textContent += stdout; } else if (stderr) { - resultPre.textContent += `[错误] ${stderr}`; + resultPre.textContent += typeof t === 'function' ? t('sshPage.logErrorMsg', { msg: stderr }) : `[错误] ${stderr}`; } else { - resultPre.textContent += '(日志为空)'; + resultPre.textContent += (typeof t === 'function' ? t('ui.logEmpty') : '(日志为空)'); } } catch (e) { - resultPre.textContent += `获取日志失败: ${e.message}`; + resultPre.textContent += typeof t === 'function' ? t('sshPage.getLogFailedMsg', { msg: e.message }) : `获取日志失败: ${e.message}`; } resultPre.scrollTop = resultPre.scrollHeight; @@ -10416,18 +10483,18 @@ async function viewServiceLog(idx, safeName) { async function stopServiceProcess(idx, safeName) { const cmd = sshCommands[selectedHostId]?.[idx]; if (!cmd) { - showToast('命令不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.cmdNotFound') : '命令不存在'), 'error'); return; } const host = window._cmdHostsList?.find(h => h.id === selectedHostId); if (!host) { - showToast('主机信息不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.hostNotFound') : '主机信息不存在'), 'error'); return; } // 确认对话框 - if (!confirm(`确定要停止服务 "${cmd.name}" 吗?`)) { + if (!confirm(typeof t === 'function' ? t('ui.confirmStopService', { name: cmd.name }) : `确定要停止服务 "${cmd.name}" 吗?`)) { return; } @@ -10440,7 +10507,7 @@ async function stopServiceProcess(idx, safeName) { document.getElementById('cancel-exec-btn').style.display = 'none'; document.getElementById('nohup-actions').style.display = 'none'; - resultPre.textContent = `停止服务: ${cmd.name}\n\n`; + resultPre.textContent = (typeof t === 'function' ? t('sshPage.stopServiceName', { name: cmd.name }) : `停止服务: ${cmd.name}`) + '\n\n'; resultSection.scrollIntoView({ behavior: 'smooth' }); try { @@ -10458,7 +10525,7 @@ async function stopServiceProcess(idx, safeName) { if (status.startsWith('RUNNING:')) { const pid = status.split(':')[1]; - resultPre.textContent += `进程运行中 (PID: ${pid}),正在停止...\n`; + resultPre.textContent += (typeof t === 'function' ? t('sshPage.processRunningPid', { pid }) : `进程运行中 (PID: ${pid}),正在停止...`) + '\n'; // 发送 SIGTERM const killResult = await api.call('ssh.exec', { @@ -10473,7 +10540,7 @@ async function stopServiceProcess(idx, safeName) { const killStatus = (killResult.data?.stdout || '').trim(); if (killStatus === 'STOPPED') { resultPre.textContent += `服务已停止\n`; - showToast('服务已停止', 'success'); + showToast((typeof t === 'function' ? t('toast.serviceStopped') : '服务已停止'), 'success'); // 更新状态变量 if (cmd.varName) { @@ -10496,19 +10563,19 @@ async function stopServiceProcess(idx, safeName) { timeout_ms: 5000 }); resultPre.textContent += `已强制终止\n`; - showToast('服务已强制停止', 'warning'); + showToast((typeof t === 'function' ? t('toast.serviceForceKilled') : '服务已强制停止'), 'warning'); updateServiceStatusInList(); } } else if (status === 'STOPPED') { resultPre.textContent += `进程已经停止\n`; - showToast('进程已经停止', 'info'); + showToast((typeof t === 'function' ? t('toast.processAlreadyStopped') : '进程已经停止'), 'info'); } else { resultPre.textContent += `PID 文件不存在,服务可能未启动\n`; - showToast('服务未运行', 'info'); + showToast((typeof t === 'function' ? t('toast.serviceNotRunning') : '服务未运行'), 'info'); } } catch (e) { resultPre.textContent += `停止服务失败: ${e.message}`; - showToast('停止服务失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.stopServiceFailedMsg', { msg: e.message }) : '停止服务失败: ' + e.message), 'error'); } resultPre.scrollTop = resultPre.scrollHeight; @@ -10520,13 +10587,13 @@ async function executeCommand(idx) { const host = window._cmdHostsList?.find(h => h.id === selectedHostId); if (!host) { - showToast('主机信息不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.hostNotFound') : '主机信息不存在'), 'error'); return; } // 检查是否有正在运行的命令(nohup 模式不需要检查) if (currentExecSessionId && !cmd.nohup) { - showToast('有命令正在执行中,请先取消或等待完成', 'warning'); + showToast((typeof t === 'function' ? t('toast.commandRunning') : '有命令正在执行中,请先取消或等待完成'), 'warning'); return; } @@ -10640,8 +10707,8 @@ async function executeCommand(idx) { currentExecSessionId = null; return; } - resultPre.textContent = `启动执行失败\n\n${e.message}`; - showToast('启动执行失败: ' + e.message, 'error'); + resultPre.textContent = typeof t === 'function' ? t('sshPage.startExecFailedDetail', { msg: e.message }) : `启动执行失败\n\n${e.message}`; + showToast((typeof t === 'function' ? t('ssh.startExecFailedMsg', { msg: e.message }) : '启动执行失败: ' + e.message), 'error'); cancelBtn.style.display = 'none'; currentExecSessionId = null; } @@ -10649,21 +10716,21 @@ async function executeCommand(idx) { async function cancelExecution() { if (!currentExecSessionId) { - showToast('没有正在执行的命令', 'info'); + showToast((typeof t === 'function' ? t('toast.noRunningCommand') : '没有正在执行的命令'), 'info'); return; } const cancelBtn = document.getElementById('cancel-exec-btn'); cancelBtn.disabled = true; - cancelBtn.textContent = '取消中...'; + cancelBtn.textContent = typeof t === 'function' ? t('otaPage.cancelling') : '取消中...'; try { await api.call('ssh.cancel', { session_id: currentExecSessionId }); - showToast('取消请求已发送', 'info'); + showToast((typeof t === 'function' ? t('toast.cancelSent') : '取消请求已发送'), 'info'); } catch (e) { - showToast('取消失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.cancelFailedMsg', { msg: e.message }) : '取消失败: ' + e.message), 'error'); cancelBtn.disabled = false; - cancelBtn.innerHTML = ' 取消 (Esc)'; + cancelBtn.innerHTML = ' ' + (typeof t === 'function' ? t('sshPage.cancelEsc') : '取消 (Esc)'); } } @@ -10726,7 +10793,7 @@ function handleSshExecMessage(msg) { if (msg.extracted) { resultPre.textContent += ` 提取内容: ${msg.extracted}\n`; } - showToast('模式匹配成功', msg.fail_matched ? 'error' : 'success'); + showToast((typeof t === 'function' ? t('toast.patternMatchSuccess') : '模式匹配成功'), msg.fail_matched ? 'error' : 'success'); } else if (isExtractOnly) { /* 仅提取更新(持续提取场景)*/ /* 不在输出区显示,只更新面板 */ @@ -10782,15 +10849,15 @@ function handleSshExecMessage(msg) { // 根据状态显示 Toast if (msg.status === 'match_success' || (msg.exit_code === 0 && !msg.fail_matched)) { - showToast('命令执行成功', 'success'); + showToast((typeof t === 'function' ? t('toast.commandSuccess') : '命令执行成功'), 'success'); } else if (msg.status === 'match_failed' || msg.fail_matched) { - showToast('命令执行完成,模式匹配失败', 'warning'); + showToast((typeof t === 'function' ? t('toast.commandMatchFailed') : '命令执行完成,模式匹配失败'), 'warning'); } else if (msg.status === 'timeout') { - showToast('命令执行超时', 'warning'); + showToast((typeof t === 'function' ? t('toast.commandTimeout') : '命令执行超时'), 'warning'); } else if (msg.exit_code === 0) { - showToast('命令执行成功', 'success'); + showToast((typeof t === 'function' ? t('toast.commandSuccess') : '命令执行成功'), 'success'); } else { - showToast(`命令执行完成,退出码: ${msg.exit_code}`, 'warning'); + showToast(typeof t === 'function' ? t('toast.commandCompletedCode', { code: msg.exit_code }) : `命令执行完成,退出码: ${msg.exit_code}`, 'warning'); } } break; @@ -10802,7 +10869,7 @@ function handleSshExecMessage(msg) { cancelBtn.style.display = 'none'; } currentExecSessionId = null; - showToast('执行出错: ' + msg.error, 'error'); + showToast((typeof t === 'function' ? t('toast.execErrorMsg', { msg: msg.error }) : '执行出错: ' + msg.error), 'error'); } break; @@ -10813,7 +10880,7 @@ function handleSshExecMessage(msg) { cancelBtn.style.display = 'none'; } currentExecSessionId = null; - showToast('命令已取消', 'info'); + showToast((typeof t === 'function' ? t('toast.commandCancelled') : '命令已取消'), 'info'); } break; } @@ -10837,7 +10904,7 @@ function updateMatchResultPanel(msg, isExtractOnly = false) { if (statusBadge) { if (isExtractOnly) { // 持续提取模式 - 显示"提取中" - statusBadge.textContent = '提取中...'; + statusBadge.textContent = typeof t === 'function' ? t('otaPage.extracting') : '提取中...'; statusBadge.className = 'match-status extracting'; } else { const statusConfig = { @@ -10861,7 +10928,7 @@ function updateMatchResultPanel(msg, isExtractOnly = false) { expectResult.textContent = msg.expect_matched ? 'true' : 'false'; expectResult.className = `match-value ${msg.expect_matched ? 'true' : 'false'}`; } else { - expectResult.textContent = '未配置'; + expectResult.textContent = typeof t === 'function' ? t('otaPage.expectPatternConfigured') : '未配置'; expectResult.className = 'match-value'; } } @@ -10870,10 +10937,10 @@ function updateMatchResultPanel(msg, isExtractOnly = false) { const failResult = document.getElementById('match-fail-result'); if (failResult) { if (msg.fail_matched !== undefined) { - failResult.textContent = msg.fail_matched ? 'true (检测到错误)' : 'false'; + failResult.textContent = msg.fail_matched ? (typeof t === 'function' ? t('sshPage.failMatchedTrue') : 'true (检测到错误)') : (typeof t === 'function' ? t('sshPage.failMatchedFalse') : 'false'); failResult.className = `match-value ${msg.fail_matched ? 'false' : 'true'}`; } else { - failResult.textContent = '未配置'; + failResult.textContent = typeof t === 'function' ? t('otaPage.expectPatternConfigured') : '未配置'; failResult.className = 'match-value'; } } @@ -10885,7 +10952,7 @@ function updateMatchResultPanel(msg, isExtractOnly = false) { extractedResult.textContent = msg.extracted; extractedResult.title = msg.extracted; } else { - extractedResult.textContent = '无'; + extractedResult.textContent = typeof t === 'function' ? t('otaPage.extractedNone') : '无'; } } @@ -11437,7 +11504,7 @@ async function refreshSecurityPage() { // 更新 SSH 测试的密钥下拉列表 if (sshKeySelect) { - sshKeySelect.innerHTML = ''; + sshKeySelect.innerHTML = ''; if (keys.data?.keys && keys.data.keys.length > 0) { keys.data.keys.forEach(key => { const option = document.createElement('option'); @@ -11660,7 +11727,7 @@ function showFullFingerprint(index) { const host = window._knownHostsList?.[index]; if (!host) return; - alert(`主机: ${host.host}:${host.port}\n类型: ${host.type}\n指纹 (SHA256):\n${host.fingerprint}`); + alert(typeof t === 'function' ? t('ui.alertHostFingerprint', { host: host.host, port: host.port, type: host.type, fingerprint: host.fingerprint }) : `主机: ${host.host}:${host.port}\n类型: ${host.type}\n指纹 (SHA256):\n${host.fingerprint}`); } /** @@ -11670,18 +11737,18 @@ async function removeKnownHost(index) { const host = window._knownHostsList?.[index]; if (!host) return; - if (!confirm(`确定要删除主机 ${host.host}:${host.port} 的指纹记录吗?\n\n删除后下次连接将重新验证服务器指纹。`)) return; + if (!confirm(typeof t === 'function' ? t('ui.confirmDeleteFingerprint', { host: host.host, port: host.port }) : `确定要删除主机 ${host.host}:${host.port} 的指纹记录吗?\n\n删除后下次连接将重新验证服务器指纹。`)) return; try { const result = await api.call('hosts.remove', { host: host.host, port: host.port }); if (result.code === 0) { - showToast('已删除主机指纹', 'success'); + showToast((typeof t === 'function' ? t('toast.hostFingerprintDeleted') : '已删除主机指纹'), 'success'); await refreshKnownHostsList(); } else { - showToast('删除失败: ' + (result.message || '未知错误'), 'error'); + showToast((typeof t === 'function' ? t('toast.deleteFailedMsg', { msg: result.message || t('common.unknown') }) : '删除失败: ' + (result.message || '未知错误')), 'error'); } } catch (e) { - showToast('删除失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.deleteFailedMsg', { msg: e.message }) : '删除失败: ' + e.message), 'error'); } } @@ -11689,7 +11756,7 @@ async function removeKnownHost(index) { * 测试 SSH 连接 */ async function testSshConnection(hostId) { - showToast(`正在测试连接 ${hostId}...`, 'info'); + showToast(typeof t === 'function' ? t('toast.testingConnection', { host: hostId }) : `正在测试连接 ${hostId}...`, 'info'); try { // 获取主机信息 @@ -11697,12 +11764,12 @@ async function testSshConnection(hostId) { console.log('ssh.hosts.get result:', hostResult); if (hostResult.code !== 0) { - showToast(`无法获取主机信息: ${hostResult.message || '未知错误'}`, 'error'); + showToast(typeof t === 'function' ? t('toast.cannotGetHostInfo') + ': ' + (hostResult.message || t('common.unknown')) : `无法获取主机信息: ${hostResult.message || '未知错误'}`, 'error'); return; } if (!hostResult.data) { - showToast('主机信息为空', 'error'); + showToast((typeof t === 'function' ? t('toast.hostInfoEmpty') : '主机信息为空'), 'error'); return; } @@ -11718,13 +11785,13 @@ async function testSshConnection(hostId) { }); if (execResult.code === 0) { - showToast(`连接 ${hostId} 成功!`, 'success'); + showToast(typeof t === 'function' ? t('toast.connectionSuccess', { host: hostId }) : `连接 ${hostId} 成功!`, 'success'); } else { - showToast(`连接失败: ${execResult.message || '未知错误'}`, 'error'); + showToast(typeof t === 'function' ? t('toast.connectFailedMsg', { msg: execResult.message || t('common.unknown') }) : `连接失败: ${execResult.message || '未知错误'}`, 'error'); } } catch (e) { console.error('Test SSH connection error:', e); - showToast(`测试失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.testConnectionFailed') + ': ' + e.message : `测试失败: ${e.message}`, 'error'); } } @@ -11734,11 +11801,11 @@ async function testSshConnection(hostId) { async function testSshHostByIndex(index) { const host = window._sshHostsList?.[index]; if (!host) { - showToast('主机信息不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.hostNotFound') : '主机信息不存在'), 'error'); return; } - showToast(`正在测试连接 ${host.id}...`, 'info'); + showToast(typeof t === 'function' ? t('toast.testingConnection', { host: host.id }) : `正在测试连接 ${host.id}...`, 'info'); try { const execResult = await api.call('ssh.exec', { @@ -11751,13 +11818,13 @@ async function testSshHostByIndex(index) { }); if (execResult.code === 0) { - showToast(`连接 ${host.id} 成功!`, 'success'); + showToast(typeof t === 'function' ? t('toast.connectionSuccess', { host: host.id }) : `连接 ${host.id} 成功!`, 'success'); } else { - showToast(`连接失败: ${execResult.message || '未知错误'}`, 'error'); + showToast(typeof t === 'function' ? t('toast.connectFailedMsg', { msg: execResult.message || t('common.unknown') }) : `连接失败: ${execResult.message || '未知错误'}`, 'error'); } } catch (e) { console.error('Test SSH connection error:', e); - showToast(`测试失败: ${e.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.testConnectionFailed') + ': ' + e.message : `测试失败: ${e.message}`, 'error'); } } @@ -11992,17 +12059,17 @@ async function previewSshHostImport() { `; if (data.exists) { - html += `
该配置已存在,导入将覆盖现有文件
`; + html += `
${typeof t === 'function' ? t('securityPage.configExistsWarning') : '该配置已存在,导入将覆盖现有文件'}
`; } previewDiv.innerHTML = html; step2.style.display = 'block'; resultBox.className = 'result-box success'; - resultBox.textContent = '签名验证通过'; + resultBox.textContent = typeof t === 'function' ? t('ssh.signatureVerified') : '签名验证通过'; importBtn.disabled = false; } else { resultBox.className = 'result-box error'; - resultBox.textContent = (result.message || '无法验证配置包'); + resultBox.textContent = (result.message || (typeof t === 'function' ? t('ssh.cannotVerifyPack') : '无法验证配置包')); } } catch (e) { resultBox.className = 'result-box error'; @@ -12019,12 +12086,12 @@ async function confirmSshHostImport() { const importBtn = document.getElementById('import-ssh-host-btn'); if (!window._importSshHostTscfg) { - showToast('请先选择文件', 'error'); + showToast((typeof t === 'function' ? t('toast.selectFileFirst') : '请先选择文件'), 'error'); return; } resultBox.classList.remove('hidden', 'success', 'error', 'warning'); - resultBox.textContent = '正在保存配置...'; + resultBox.textContent = typeof t === 'function' ? t('ssh.savingConfig') : '正在保存配置...'; importBtn.disabled = true; try { @@ -12065,22 +12132,22 @@ async function confirmSshHostImport() { async function removeHostByIndex(index) { const host = window._sshHostsList?.[index]; if (!host) { - showToast('主机信息不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.hostNotFound') : '主机信息不存在'), 'error'); return; } - if (!confirm(`确定要从列表中移除主机 "${host.id}" 吗?\n\n注意:这只会移除本地记录,不会删除已部署到服务器上的公钥。如需撤销公钥,请点击「撤销」按钮。`)) return; + if (!confirm(typeof t === 'function' ? t('ui.confirmRemoveHostLocal', { id: host.id }) : `确定要从列表中移除主机 "${host.id}" 吗?\n\n注意:这只会移除本地记录,不会删除已部署到服务器上的公钥。如需撤销公钥,请点击「撤销」按钮。`)) return; try { const result = await api.call('ssh.hosts.remove', { id: host.id }); if (result.code === 0) { - showToast(`SSH 主机 ${host.id} 已从列表移除`, 'success'); + showToast(typeof t === 'function' ? t('toast.sshHostRemoved', { id: host.id }) : `SSH 主机 ${host.id} 已从列表移除`, 'success'); await loadSshHostsData(); } else { - showToast('移除失败: ' + (result.message || '未知错误'), 'error'); + showToast((typeof t === 'function' ? t('toast.removeFailedMsg', { msg: result.message || t('common.unknown') }) : '移除失败: ' + (result.message || '未知错误')), 'error'); } } catch (e) { - showToast('移除失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.removeFailedMsg', { msg: e.message }) : '移除失败: ' + e.message), 'error'); } } @@ -12090,7 +12157,7 @@ async function removeHostByIndex(index) { function revokeKeyFromHost(index) { const host = window._sshHostsList?.[index]; if (!host) { - showToast('主机信息不存在', 'error'); + showToast((typeof t === 'function' ? t('sshPage.hostNotFound') : '主机信息不存在'), 'error'); return; } @@ -12195,29 +12262,29 @@ async function doRevokeFromHost(index) { * 从安全页面删除 SSH 主机(保留兼容性) */ async function deleteSshHostFromSecurity(id) { - if (!confirm(`确定要从列表中移除主机 "${id}" 吗?\n\n注意:这只会移除本地记录,不会删除已部署到服务器上的公钥。如需撤销公钥,请使用密钥管理中的「撤销」功能。`)) return; + if (!confirm(typeof t === 'function' ? t('ui.confirmRemoveHostLocal2', { id }) : `确定要从列表中移除主机 "${id}" 吗?\n\n注意:这只会移除本地记录,不会删除已部署到服务器上的公钥。如需撤销公钥,请使用密钥管理中的「撤销」功能。`)) return; try { const result = await api.call('ssh.hosts.remove', { id }); if (result.code === 0) { - showToast(`SSH 主机 ${id} 已从列表移除`, 'success'); + showToast(typeof t === 'function' ? t('toast.sshHostRemoved', { id }) : `SSH 主机 ${id} 已从列表移除`, 'success'); await loadSshHostsData(); } else { - showToast('移除失败: ' + (result.message || '未知错误'), 'error'); + showToast((typeof t === 'function' ? t('toast.removeFailedMsg', { msg: result.message || t('common.unknown') }) : '移除失败: ' + (result.message || '未知错误')), 'error'); } } catch (e) { - showToast('移除失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.removeFailedMsg', { msg: e.message }) : '移除失败: ' + e.message), 'error'); } } async function deleteKey(id) { - if (confirm(`确定要删除密钥 "${id}" 吗?此操作不可撤销!`)) { + if (confirm(typeof t === 'function' ? t('ui.confirmDeleteKey', { id }) : `确定要删除密钥 "${id}" 吗?此操作不可撤销!`)) { try { await api.keyDelete(id); - showToast('密钥已删除', 'success'); + showToast((typeof t === 'function' ? t('toast.keyDeleted') : '密钥已删除'), 'success'); await refreshSecurityPage(); } catch (e) { - showToast('删除失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.deleteFailedMsg', { msg: e.message }) : '删除失败: ' + e.message), 'error'); } } } @@ -12229,16 +12296,16 @@ async function exportKey(id) { // 显示公钥弹窗 showPubkeyModal(id, result.data.public_key, result.data.type, result.data.comment); } else { - showToast('无法获取公钥', 'error'); + showToast((typeof t === 'function' ? t('toast.cannotGetPublicKey') : '无法获取公钥'), 'error'); } } catch (e) { - showToast('导出失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.exportFailedMsg', { msg: e.message }) : '导出失败: ' + e.message), 'error'); } } async function exportPrivateKey(id) { // 安全确认 - if (!confirm(`安全警告\n\n您正在导出私钥 "${id}"。\n\n私钥是高度敏感的安全凭证,请确保:\n• 不要在公共网络传输\n• 不要分享给他人\n• 安全存储在本地\n\n确定要继续吗?`)) { + if (!confirm(typeof t === 'function' ? t('ui.confirmExportPrivateKey', { id }) : `安全警告\n\n您正在导出私钥 "${id}"。\n\n私钥是高度敏感的安全凭证,请确保:\n• 不要在公共网络传输\n• 不要分享给他人\n• 安全存储在本地\n\n确定要继续吗?`)) { return; } @@ -12247,10 +12314,10 @@ async function exportPrivateKey(id) { if (result.data?.private_key) { showPrivkeyModal(id, result.data.private_key, result.data.type, result.data.comment); } else { - showToast('无法获取私钥', 'error'); + showToast((typeof t === 'function' ? t('toast.cannotGetPrivateKey') : '无法获取私钥'), 'error'); } } catch (e) { - showToast('导出失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.exportFailedMsg', { msg: e.message }) : '导出失败: ' + e.message), 'error'); } } @@ -12335,12 +12402,12 @@ async function copyPubkey() { if (textarea) { try { await navigator.clipboard.writeText(textarea.value); - showToast('已复制到剪贴板', 'success'); + showToast((typeof t === 'function' ? t('toast.copied') : '已复制到剪贴板'), 'success'); } catch (e) { // Fallback for older browsers textarea.select(); document.execCommand('copy'); - showToast('已复制到剪贴板', 'success'); + showToast((typeof t === 'function' ? t('toast.copied') : '已复制到剪贴板'), 'success'); } } } @@ -12356,7 +12423,7 @@ function downloadPubkey(id) { document.body.appendChild(a); a.click(); document.body.removeChild(a); - showToast(`已下载 ${id}.pub`, 'success'); + showToast(typeof t === 'function' ? t('toast.downloadedPublicKey', { id }) : `已下载 ${id}.pub`, 'success'); } } @@ -12365,11 +12432,11 @@ async function copyPrivkey() { if (textarea) { try { await navigator.clipboard.writeText(textarea.value); - showToast('已复制到剪贴板', 'success'); + showToast((typeof t === 'function' ? t('toast.copied') : '已复制到剪贴板'), 'success'); } catch (e) { textarea.select(); document.execCommand('copy'); - showToast('已复制到剪贴板', 'success'); + showToast((typeof t === 'function' ? t('toast.copied') : '已复制到剪贴板'), 'success'); } } } @@ -12385,7 +12452,7 @@ function downloadPrivkey(id) { document.body.appendChild(a); a.click(); document.body.removeChild(a); - showToast(`已下载 ${id}`, 'success'); + showToast(typeof t === 'function' ? t('toast.downloadedPrivateKey', { id }) : `已下载 ${id}`, 'success'); } } @@ -12421,7 +12488,7 @@ async function deployKey() { const password = document.getElementById('deploy-password').value; if (!host || !user || !password) { - showToast('请填写完整的服务器信息', 'error'); + showToast((typeof t === 'function' ? t('toast.fillServerInfo') : '请填写完整的服务器信息'), 'error'); return; } @@ -12429,7 +12496,7 @@ async function deployKey() { const deployBtn = document.getElementById('deploy-btn'); resultBox.classList.remove('hidden', 'success', 'error'); - resultBox.textContent = '正在部署密钥...'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.deployingKey') : '正在部署密钥...'; deployBtn.disabled = true; try { @@ -12445,14 +12512,14 @@ async function deployKey() { } resultBox.textContent = msg; resultBox.classList.add('success'); - showToast('密钥部署成功', 'success'); + showToast((typeof t === 'function' ? t('toast.keyDeployed') : '密钥部署成功'), 'success'); // 刷新已部署主机列表(后端 ssh.copyid 会自动注册主机) await loadSshHostsData(); } else { throw new Error('部署失败'); } } catch (e) { - resultBox.textContent = '部署失败: ' + e.message; + resultBox.textContent = (typeof t === 'function' ? t('securityPage.deployFailedMsg', { msg: e.message }) : '部署失败: ' + e.message); resultBox.classList.add('error'); } finally { deployBtn.disabled = false; @@ -12491,7 +12558,7 @@ async function revokeKey() { const password = document.getElementById('revoke-password').value; if (!host || !user || !password) { - showToast('请填写完整的服务器信息', 'error'); + showToast((typeof t === 'function' ? t('toast.fillServerInfo') : '请填写完整的服务器信息'), 'error'); return; } @@ -12499,7 +12566,7 @@ async function revokeKey() { const revokeBtn = document.getElementById('revoke-btn'); resultBox.classList.remove('hidden', 'success', 'error'); - resultBox.textContent = '正在撤销密钥...'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.revokingKey') : '正在撤销密钥...'; revokeBtn.disabled = true; try { @@ -12507,18 +12574,18 @@ async function revokeKey() { const result = await api.sshRevoke(host, user, password, currentRevokeKeyId, port); if (result.data?.revoked) { - resultBox.textContent = `撤销成功!已从 ${user}@${host} 移除 ${result.data.removed_count || 1} 个匹配的公钥`; + resultBox.textContent = typeof t === 'function' ? t('sshPage.revokeSuccess', { target: `${user}@${host}`, count: result.data.removed_count || 1 }) : `撤销成功!已从 ${user}@${host} 移除 ${result.data.removed_count || 1} 个匹配的公钥`; resultBox.classList.add('success'); - showToast('密钥撤销成功', 'success'); + showToast((typeof t === 'function' ? t('toast.keyRevoked') : '密钥撤销成功'), 'success'); } else if (result.data?.found === false) { - resultBox.textContent = `该公钥未在 ${user}@${host} 上找到`; + resultBox.textContent = typeof t === 'function' ? t('sshPage.keyNotFound', { target: `${user}@${host}` }) : `该公钥未在 ${user}@${host} 上找到`; resultBox.classList.add('warning'); - showToast('公钥未找到', 'warning'); + showToast((typeof t === 'function' ? t('toast.publicKeyNotFound') : '公钥未找到'), 'warning'); } else { throw new Error('撤销失败'); } } catch (e) { - resultBox.textContent = '撤销失败: ' + e.message; + resultBox.textContent = (typeof t === 'function' ? t('securityPage.revokeFailedMsg', { msg: e.message }) : '撤销失败: ' + e.message); resultBox.classList.add('error'); } finally { revokeBtn.disabled = false; @@ -12548,34 +12615,34 @@ async function removeAndRetry() { try { // 使用新的 hosts.update API 更新主机密钥 await api.hostsUpdate(currentMismatchInfo.host, currentMismatchInfo.port || 22); - showToast('旧主机密钥已移除,请重新连接以信任新密钥', 'success'); + showToast((typeof t === 'function' ? t('toast.oldHostKeyRemoved') : '旧主机密钥已移除,请重新连接以信任新密钥'), 'success'); hideHostMismatchModal(); await refreshSecurityPage(); } catch (e) { - showToast('更新失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.updateFailedMsg', { msg: e.message }) : '更新失败: ' + e.message), 'error'); } } async function removeHost(host, port) { - if (confirm(`确定要移除主机 "${host}:${port}" 的记录吗?`)) { + if (confirm(typeof t === 'function' ? t('ui.confirmRemoveKnownHost', { host, port }) : `确定要移除主机 "${host}:${port}" 的记录吗?`)) { try { await api.hostsRemove(host, port); - showToast('主机已移除', 'success'); + showToast((typeof t === 'function' ? t('toast.hostRemoved') : '主机已移除'), 'success'); await refreshSecurityPage(); } catch (e) { - showToast('移除失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.removeFailedMsg', { msg: e.message }) : '移除失败: ' + e.message), 'error'); } } } async function clearAllHosts() { - if (confirm('确定要清除所有已知主机记录吗?此操作不可撤销!')) { + if (confirm(typeof t === 'function' ? t('automation.confirmClearKnownHosts') : '确定要清除所有已知主机记录吗?此操作不可撤销!')) { try { await api.hostsClear(); - showToast('已清除所有已知主机', 'success'); + showToast((typeof t === 'function' ? t('toast.allHostsCleared') : '已清除所有已知主机'), 'success'); await refreshSecurityPage(); } catch (e) { - showToast('清除失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.clearFailedMsg', { msg: e.message }) : '清除失败: ' + e.message), 'error'); } } } @@ -12670,16 +12737,16 @@ async function loadConfigPackCert() { loading.style.display = 'none'; content.classList.remove('hidden'); } catch (e) { - loading.textContent = '加载失败: ' + e.message; + loading.textContent = typeof t === 'function' ? t('securityPage.loadFailedMsg', { msg: e.message }) : '加载失败: ' + e.message; } } function copyPackCertToClipboard() { const pem = document.getElementById('pack-cert-pem').value; navigator.clipboard.writeText(pem).then(() => { - showToast('证书已复制到剪贴板', 'success'); + showToast(typeof t === 'function' ? t('toast.certCopied') : '证书已复制到剪贴板', 'success'); }).catch(e => { - showToast('复制失败: ' + e.message, 'error'); + showToast(typeof t === 'function' ? t('toast.copyFailedMsg', { msg: e.message }) : '复制失败: ' + e.message, 'error'); }); } @@ -12714,13 +12781,13 @@ async function verifyConfigPack() { if (!content) { resultBox.className = 'result-box error'; - resultBox.textContent = '请上传文件或粘贴配置包内容'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.uploadOrPasteContent') : '请上传文件或粘贴配置包内容'; resultBox.classList.remove('hidden'); return; } resultBox.className = 'result-box'; - resultBox.textContent = '验证中...'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.verifying') : '验证中...'; resultBox.classList.remove('hidden'); preview.classList.add('hidden'); @@ -12731,7 +12798,7 @@ async function verifyConfigPack() { const data = result.data; if (data.valid) { resultBox.className = 'result-box success'; - resultBox.innerHTML = '签名验证通过'; + resultBox.innerHTML = typeof t === 'function' ? t('ssh.signatureVerified') : '签名验证通过'; // 显示签名信息 if (data.signature) { @@ -12748,11 +12815,11 @@ async function verifyConfigPack() { } } else { resultBox.className = 'result-box error'; - resultBox.textContent = '验证失败: ' + (data.result_message || '签名无效'); + resultBox.textContent = (typeof t === 'function' ? t('securityPage.verifyFailed') : '验证失败') + ': ' + (data.result_message || (typeof t === 'function' ? t('securityPage.signatureInvalid') : '签名无效')); } } catch (e) { resultBox.className = 'result-box error'; - resultBox.textContent = '验证失败: ' + e.message; + resultBox.textContent = (typeof t === 'function' ? t('securityPage.verifyFailed') : '验证失败') + ': ' + e.message; } } @@ -12763,13 +12830,13 @@ async function importConfigPack() { if (!content) { resultBox.className = 'result-box error'; - resultBox.textContent = '请上传文件或粘贴配置包内容'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.uploadOrPasteContent') : '请上传文件或粘贴配置包内容'; resultBox.classList.remove('hidden'); return; } resultBox.className = 'result-box'; - resultBox.textContent = '导入中...'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.importing') : '导入中...'; resultBox.classList.remove('hidden'); try { @@ -12859,7 +12926,7 @@ function closeConfigPackApplyConfirm() { */ async function applyConfigPackFromPath(path) { closeConfigPackApplyConfirm(); - showToast('正在应用配置...', 'info'); + showToast(typeof t === 'function' ? t('toast.applyingConfig') : '正在应用配置...', 'info'); try { const result = await api.call('config.pack.apply', { path }, 'POST'); @@ -12871,13 +12938,13 @@ async function applyConfigPackFromPath(path) { if (data.success) { const modules = data.applied_modules || []; const moduleList = modules.length > 0 ? modules.join(', ') : '无'; - showToast(`配置已应用\n模块: ${moduleList}`, 'success', 5000); + showToast(typeof t === 'function' ? t('toast.configApplied', { modules: moduleList }) : `配置已应用\n模块: ${moduleList}`, 'success', 5000); } else { - showToast(`应用失败: ${data.result_message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.applyFailedMsg', { msg: data.result_message }) : `应用失败: ${data.result_message}`, 'error'); } } catch (e) { console.error('Apply config pack error:', e); - showToast('应用失败: ' + e.message, 'error'); + showToast(typeof t === 'function' ? t('toast.applyFailedMsg', { msg: e.message }) : '应用失败: ' + e.message, 'error'); } } @@ -13126,7 +13193,7 @@ function packExportDeselectAll() { async function packExportSelectDir() { // 与全选功能相同,但可以在 UI 上有区分 await packExportSelectAll(); - showToast(`已选择当前目录下的所有 JSON 文件`, 'success'); + showToast(typeof t === 'function' ? t('toast.selectedJsonFiles') : '已选择当前目录下的所有 JSON 文件', 'success'); } // 文件大小格式化 @@ -13153,7 +13220,7 @@ async function exportConfigPack() { if (!name) { resultBox.className = 'result-box error'; resultBox.style.visibility = 'visible'; - resultBox.textContent = '请输入配置名称'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.enterConfigName') : '请输入配置名称'; return; } @@ -13162,14 +13229,14 @@ async function exportConfigPack() { if (okFiles.length === 0) { resultBox.className = 'result-box error'; resultBox.style.visibility = 'visible'; - resultBox.textContent = '请选择配置文件'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.selectConfigFile') : '请选择配置文件'; return; } if (!recipientCert) { resultBox.className = 'result-box error'; resultBox.style.visibility = 'visible'; - resultBox.textContent = '请粘贴目标设备证书'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.pasteTargetCert') : '请粘贴目标设备证书'; return; } @@ -13190,13 +13257,13 @@ async function exportConfigPack() { } catch (e) { resultBox.className = 'result-box error'; resultBox.style.visibility = 'visible'; - resultBox.textContent = '配置文件不是有效的 JSON: ' + e.message; + resultBox.textContent = (typeof t === 'function' ? t('securityPage.invalidJsonConfigMsg', { msg: e.message }) : '配置文件不是有效的 JSON: ' + e.message); return; } resultBox.className = 'result-box'; resultBox.style.visibility = 'visible'; - resultBox.textContent = `生成配置包中 (${okFiles.length} 个文件)...`; + resultBox.textContent = typeof t === 'function' ? t('securityPage.generatingPackWithCount', { count: okFiles.length }) : `生成配置包中 (${okFiles.length} 个文件)...`; document.getElementById('pack-export-tscfg').value = ''; try { @@ -13233,7 +13300,7 @@ async function exportConfigPack() { // 显示保存路径 const savedPathSpan = document.getElementById('pack-export-saved-path'); if (savedPath && savedPathSpan) { - savedPathSpan.textContent = `已保存到设备`; + savedPathSpan.textContent = typeof t === 'function' ? t('securityPage.savedToDevice') : '已保存到设备'; savedPathSpan.style.display = 'inline'; } @@ -13248,16 +13315,16 @@ async function exportConfigPack() { } catch (e) { console.error('[ConfigPack] Export error:', e); resultBox.className = 'result-box error'; - resultBox.textContent = '生成失败: ' + e.message; + resultBox.textContent = (typeof t === 'function' ? t('securityPage.generationFailedMsg', { msg: e.message }) : '生成失败: ' + e.message); } } function copyPackTscfgToClipboard() { const tscfg = document.getElementById('pack-export-tscfg').value; navigator.clipboard.writeText(tscfg).then(() => { - showToast('配置包已复制到剪贴板', 'success'); + showToast(typeof t === 'function' ? t('toast.configPackCopied') : '配置包已复制到剪贴板', 'success'); }).catch(e => { - showToast('复制失败: ' + e.message, 'error'); + showToast(typeof t === 'function' ? t('toast.copyFailedMsg', { msg: e.message }) : '复制失败: ' + e.message, 'error'); }); } @@ -13275,7 +13342,7 @@ function downloadPackTscfg() { document.body.removeChild(a); URL.revokeObjectURL(url); - showToast('配置包已下载: ' + filename, 'success'); + showToast(typeof t === 'function' ? t('toast.configPackDownloaded', { filename }) : '配置包已下载: ' + filename, 'success'); } // 配置包:列表弹窗 @@ -13526,15 +13593,15 @@ async function generateCertKeypair() { const force = window._certPkiStatus?.has_private_key; resultBox.classList.remove('hidden', 'success', 'error'); - resultBox.textContent = '正在生成密钥对...'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.generatingKeyPair') : '正在生成密钥对...'; btn.disabled = true; try { const result = await api.certGenerateKeypair(force); if (result.code === 0 || result.data?.success) { - resultBox.textContent = 'ECDSA P-256 密钥对生成成功!'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.ecdsaKeyPairSuccess') : 'ECDSA P-256 密钥对生成成功!'; resultBox.classList.add('success'); - showToast('密钥对生成成功', 'success'); + showToast(typeof t === 'function' ? t('toast.keypairGenerated') : '密钥对生成成功', 'success'); setTimeout(() => { hideCertGenKeyModal(); @@ -13544,7 +13611,7 @@ async function generateCertKeypair() { throw new Error(result.message || '生成失败'); } } catch (e) { - resultBox.textContent = '生成失败: ' + e.message; + resultBox.textContent = (typeof t === 'function' ? t('securityPage.generationFailedMsg', { msg: e.message }) : '生成失败: ' + e.message); resultBox.classList.add('error'); } finally { btn.disabled = false; @@ -13573,7 +13640,7 @@ async function generateCSR() { const btn = document.getElementById('csr-gen-btn'); resultBox.classList.remove('hidden', 'success', 'error'); - resultBox.textContent = '正在生成 CSR...'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.generatingCsr') : '正在生成 CSR...'; btn.disabled = true; try { @@ -13587,12 +13654,12 @@ async function generateCSR() { resultBox.classList.add('hidden'); csrResultBox.classList.remove('hidden'); document.getElementById('csr-pem-output').value = result.data.csr_pem; - showToast('CSR 生成成功', 'success'); + showToast(typeof t === 'function' ? t('toast.csrGenerated') : 'CSR 生成成功', 'success'); } else { throw new Error(result.message || '生成失败'); } } catch (e) { - resultBox.textContent = '生成失败: ' + e.message; + resultBox.textContent = (typeof t === 'function' ? t('securityPage.generationFailedMsg', { msg: e.message }) : '生成失败: ' + e.message); resultBox.classList.add('error'); } finally { btn.disabled = false; @@ -13602,9 +13669,9 @@ async function generateCSR() { function copyCSRToClipboard() { const csr = document.getElementById('csr-pem-output').value; navigator.clipboard.writeText(csr).then(() => { - showToast('CSR 已复制到剪贴板', 'success'); + showToast(typeof t === 'function' ? t('toast.csrCopied') : 'CSR 已复制到剪贴板', 'success'); }).catch(e => { - showToast('复制失败: ' + e.message, 'error'); + showToast(typeof t === 'function' ? t('toast.copyFailedMsg', { msg: e.message }) : '复制失败: ' + e.message, 'error'); }); } @@ -13622,20 +13689,20 @@ function hideCertInstallModal() { async function installCertificate() { const certPem = document.getElementById('cert-pem-input').value.trim(); if (!certPem) { - showToast('请输入证书 PEM', 'error'); + showToast(typeof t === 'function' ? t('toast.enterCertPem') : '请输入证书 PEM', 'error'); return; } const resultBox = document.getElementById('cert-install-result'); resultBox.classList.remove('hidden', 'success', 'error'); - resultBox.textContent = '正在安装证书...'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.installingCert') : '正在安装证书...'; try { const result = await api.certInstall(certPem); if (result.code === 0 || result.data?.success) { - resultBox.textContent = '证书安装成功!'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.certInstalledSuccess') : '证书安装成功!'; resultBox.classList.add('success'); - showToast('证书安装成功', 'success'); + showToast(typeof t === 'function' ? t('toast.certInstalled') : '证书安装成功', 'success'); setTimeout(() => { hideCertInstallModal(); @@ -13645,7 +13712,7 @@ async function installCertificate() { throw new Error(result.message || '安装失败'); } } catch (e) { - resultBox.textContent = '安装失败: ' + e.message; + resultBox.textContent = (typeof t === 'function' ? t('securityPage.installFailedMsg', { msg: e.message }) : '安装失败: ' + e.message); resultBox.classList.add('error'); } } @@ -13664,20 +13731,20 @@ function hideCertInstallCAModal() { async function installCAChain() { const caPem = document.getElementById('ca-pem-input').value.trim(); if (!caPem) { - showToast('请输入 CA 证书链 PEM', 'error'); + showToast(typeof t === 'function' ? t('toast.enterCaPem') : '请输入 CA 证书链 PEM', 'error'); return; } const resultBox = document.getElementById('ca-install-result'); resultBox.classList.remove('hidden', 'success', 'error'); - resultBox.textContent = '正在安装 CA 证书链...'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.installingCaCert') : '正在安装 CA 证书链...'; try { const result = await api.certInstallCA(caPem); if (result.code === 0 || result.data?.success) { - resultBox.textContent = 'CA 证书链安装成功!'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.caCertInstalledSuccess') : 'CA 证书链安装成功!'; resultBox.classList.add('success'); - showToast('CA 证书链安装成功', 'success'); + showToast(typeof t === 'function' ? t('toast.caInstalled') : 'CA 证书链安装成功', 'success'); setTimeout(() => { hideCertInstallCAModal(); @@ -13687,7 +13754,7 @@ async function installCAChain() { throw new Error(result.message || '安装失败'); } } catch (e) { - resultBox.textContent = '安装失败: ' + e.message; + resultBox.textContent = (typeof t === 'function' ? t('securityPage.installFailedMsg', { msg: e.message }) : '安装失败: ' + e.message); resultBox.classList.add('error'); } } @@ -13711,7 +13778,7 @@ async function showCertViewModal() { throw new Error(result.message || '获取证书失败'); } } catch (e) { - loading.textContent = '加载失败: ' + e.message; + loading.textContent = typeof t === 'function' ? t('securityPage.loadFailedMsg', { msg: e.message }) : '加载失败: ' + e.message; } } @@ -13722,27 +13789,27 @@ function hideCertViewModal() { function copyCertToClipboard() { const cert = document.getElementById('cert-view-pem').value; navigator.clipboard.writeText(cert).then(() => { - showToast('证书已复制到剪贴板', 'success'); + showToast(typeof t === 'function' ? t('toast.certCopied') : '证书已复制到剪贴板', 'success'); }).catch(e => { - showToast('复制失败: ' + e.message, 'error'); + showToast(typeof t === 'function' ? t('toast.copyFailedMsg', { msg: e.message }) : '复制失败: ' + e.message, 'error'); }); } async function deleteCertCredentials() { - if (!confirm('确定要删除所有 PKI 凭证吗?\n\n这将删除:\n• 私钥\n• 设备证书\n• CA 证书链\n\n此操作不可撤销!')) { + if (!confirm(typeof t === 'function' ? t('automation.confirmDeletePKI') : '确定要删除所有 PKI 凭证吗?\n\n这将删除:\n• 私钥\n• 设备证书\n• CA 证书链\n\n此操作不可撤销!')) { return; } try { const result = await api.certDelete(); if (result.code === 0 || result.data?.success) { - showToast('PKI 凭证已删除', 'success'); + showToast(typeof t === 'function' ? t('toast.pkiDeleted') : 'PKI 凭证已删除', 'success'); await refreshCertStatus(); } else { throw new Error(result.message || '删除失败'); } } catch (e) { - showToast('删除失败: ' + e.message, 'error'); + showToast((typeof t === 'function' ? t('toast.deleteFailedMsg', { msg: e.message }) : '删除失败: ' + e.message), 'error'); } } @@ -13767,18 +13834,18 @@ async function generateKey() { const hidden = document.getElementById('keygen-hidden').checked; if (!id) { - showToast('请输入密钥 ID', 'error'); + showToast(typeof t === 'function' ? t('toast.enterKeyId') : '请输入密钥 ID', 'error'); return; } try { - showToast('正在生成密钥...', 'info'); + showToast(typeof t === 'function' ? t('toast.generatingKey') : '正在生成密钥...', 'info'); await api.keyGenerate(id, type, comment, exportable, alias, hidden); hideGenerateKeyModal(); - showToast(`密钥 "${alias || id}" 生成成功`, 'success'); + showToast(typeof t === 'function' ? t('toast.keyGenerated', { name: alias || id }) : `密钥 "${alias || id}" 生成成功`, 'success'); await refreshSecurityPage(); } catch (e) { - showToast('生成失败: ' + e.message, 'error'); + showToast(typeof t === 'function' ? t('toast.generateFailedMsg', { msg: e.message }) : '生成失败: ' + e.message, 'error'); } } @@ -14116,7 +14183,7 @@ function terminalClear() { function terminalDisconnect() { if (webTerminal) { webTerminal.disconnect(); - showToast('终端已断开', 'info'); + showToast(typeof t === 'function' ? t('toast.terminalDisconnected') : '终端已断开', 'info'); } } @@ -14245,14 +14312,14 @@ function renderModalLogs() { // 更新统计 const statsElem = document.getElementById('modal-log-stats'); if (statsElem) { - statsElem.textContent = `显示 ${filtered.length}/${modalLogEntries.length} 条`; + statsElem.textContent = typeof t === 'function' ? t('ui.displayStats', { filtered: filtered.length, total: modalLogEntries.length }) : `显示 ${filtered.length}/${modalLogEntries.length} 条`; } if (filtered.length === 0) { container.innerHTML = `
-
暂无日志
+
${typeof t === 'function' ? t('fanPage.noLogs') : '暂无日志'}
`; return; @@ -15093,7 +15160,7 @@ function displayPartitionsCompact(data) { `; } - container.innerHTML = html || '

无分区信息

'; + container.innerHTML = html || '

' + (typeof t === 'function' ? t('otaPage.noPartitionInfo') : '无分区信息') + '

'; } async function refreshOtaInfo() { @@ -15166,7 +15233,7 @@ async function refreshOtaProgress() { // 处理 App OTA 完成 - 开始 WWW OTA if (otaStep === 'app' && (state === 'pending_reboot' || state === 'completed') && wwwOtaEnabled) { - stateEl.textContent = '固件升级完成,准备升级 WebUI...'; + stateEl.textContent = typeof t === 'function' ? t('otaPage.firmwareUpgradeComplete') : '固件升级完成,准备升级 WebUI...'; await startWwwOta(); return; } @@ -15179,7 +15246,7 @@ async function refreshOtaProgress() { otaStep = 'idle'; // 显示重启倒计时 - stateEl.textContent = '全部升级完成'; + stateEl.textContent = typeof t === 'function' ? t('otaPage.allUpgradeComplete') : '全部升级完成'; document.getElementById('ota-message').innerHTML = `

固件和 WebUI 升级完成,设备正在重启...

@@ -15197,7 +15264,7 @@ async function refreshOtaProgress() { // 开始检测设备重启 startRebootDetection(); } else if (state === 'error') { - showToast('升级失败: ' + message, 'error'); + showToast(typeof t === 'function' ? t('toast.upgradeFailedMsg', { msg: message }) : '升级失败: ' + message, 'error'); clearInterval(refreshInterval); refreshInterval = null; otaStep = 'idle'; @@ -15257,7 +15324,7 @@ async function startWwwOta() { otaStep = 'www'; - document.getElementById('ota-state-text').textContent = '[2/2] 开始升级 WebUI...'; + document.getElementById('ota-state-text').textContent = typeof t === 'function' ? t('otaPage.step2Webui') : '[2/2] 开始升级 WebUI...'; document.getElementById('ota-progress-bar').style.width = '0%'; document.getElementById('ota-progress-percent').textContent = '0%'; document.getElementById('ota-message').textContent = wwwSource; @@ -15280,13 +15347,13 @@ async function startWwwOta() { sdcardOtaSource = ''; // 重置 if (result.code !== 0) { - showToast('WebUI 升级启动失败: ' + result.message, 'error'); + showToast(typeof t === 'function' ? t('toast.webuiUpgradeStartFailed') + ': ' + result.message : 'WebUI 升级启动失败: ' + result.message, 'error'); // 即使 www 失败也继续重启(因为 app 已经更新) otaStep = 'idle'; clearInterval(refreshInterval); refreshInterval = null; - document.getElementById('ota-state-text').textContent = '固件升级完成(WebUI 跳过)'; + document.getElementById('ota-state-text').textContent = typeof t === 'function' ? t('otaPage.firmwareOnlyComplete') : '固件升级完成(WebUI 跳过)'; document.getElementById('ota-message').innerHTML = `

固件已更新,WebUI 升级跳过,设备正在重启...

@@ -15325,7 +15392,7 @@ function startRebootDetection() { const countdownEl = document.getElementById('reboot-countdown'); if (countdownEl) { - countdownEl.textContent = `已等待 ${elapsed} 秒...`; + countdownEl.textContent = typeof t === 'function' ? t('ui.waitedSeconds', { elapsed }) : `已等待 ${elapsed} 秒...`; } try { @@ -15345,7 +15412,7 @@ function startRebootDetection() { `; } - showToast(`OTA 升级成功!当前版本: ${newVersion}`, 'success'); + showToast(typeof t === 'function' ? t('toast.otaUpgradeSuccess', { version: newVersion }) : `OTA 升级成功!当前版本: ${newVersion}`, 'success'); // 3 秒后刷新页面 setTimeout(() => { @@ -15361,10 +15428,10 @@ function startRebootDetection() { if (countdownEl) { countdownEl.innerHTML = ` - 等待超时 -
请手动检查设备状态并刷新页面 + ${typeof t === 'function' ? t('otaPage.waitTimeout') : '等待超时'} +
${typeof t === 'function' ? t('otaPage.checkDeviceManually') : '请手动检查设备状态并刷新页面'}
+ style="margin-top:10px">${typeof t === 'function' ? t('otaPage.refreshPage') : '刷新页面'} `; } } @@ -15375,13 +15442,13 @@ function startRebootDetection() { async function otaFromUrl() { const url = document.getElementById('ota-url-input').value.trim(); if (!url) { - showToast('请输入固件 URL', 'error'); + showToast(typeof t === 'function' ? t('toast.enterFirmwareUrl') : '请输入固件 URL', 'error'); return; } // 允许 http 和 https if (!url.startsWith('http://') && !url.startsWith('https://')) { - showToast('URL 必须以 http:// 或 https:// 开头', 'error'); + showToast(typeof t === 'function' ? t('toast.urlMustHttp') : 'URL 必须以 http:// 或 https:// 开头', 'error'); return; } @@ -15401,20 +15468,20 @@ async function otaFromUrl() { // 立即显示进度区域,提供即时反馈 const progressSection = document.getElementById('ota-progress-section'); progressSection.style.display = 'block'; - document.getElementById('ota-state-text').textContent = '[1/2] 正在连接服务器...'; + document.getElementById('ota-state-text').textContent = typeof t === 'function' ? t('otaPage.step1Connecting') : '[1/2] 正在连接服务器...'; document.getElementById('ota-progress-bar').style.width = '0%'; document.getElementById('ota-progress-percent').textContent = '0%'; - document.getElementById('ota-progress-size').textContent = '准备中...'; + document.getElementById('ota-progress-size').textContent = typeof t === 'function' ? t('otaPage.preparing') : '准备中...'; document.getElementById('ota-message').textContent = url; document.getElementById('ota-abort-btn').style.display = 'inline-block'; try { - showToast('开始两步升级:固件 + WebUI', 'info'); + showToast(typeof t === 'function' ? t('toast.twoStepUpgrade') : '开始两步升级:固件 + WebUI', 'info'); const result = await api.call('ota.upgrade_url', params); if (result.code === 0) { - showToast('固件升级已启动', 'success'); - document.getElementById('ota-state-text').textContent = '下载中...'; + showToast(typeof t === 'function' ? t('toast.firmwareUpgradeStarted') : '固件升级已启动', 'success'); + document.getElementById('ota-state-text').textContent = typeof t === 'function' ? t('otaPage.downloading') : '下载中...'; // 开始刷新进度 if (!refreshInterval) { refreshInterval = setInterval(refreshOtaProgress, 1000); @@ -15422,15 +15489,15 @@ async function otaFromUrl() { // 立即刷新一次 await refreshOtaProgress(); } else { - showToast('启动升级失败: ' + result.message, 'error'); + showToast(typeof t === 'function' ? t('toast.upgradeStartFailedMsg', { msg: result.message }) : '启动升级失败: ' + result.message, 'error'); // 显示错误状态 - document.getElementById('ota-state-text').textContent = '错误'; + document.getElementById('ota-state-text').textContent = typeof t === 'function' ? t('otaPage.stateError') : '错误'; document.getElementById('ota-message').textContent = result.message || '启动失败'; document.getElementById('ota-abort-btn').style.display = 'none'; } } catch (error) { - showToast('启动升级失败: ' + error.message, 'error'); - document.getElementById('ota-state-text').textContent = '错误'; + showToast(typeof t === 'function' ? t('toast.upgradeStartFailedMsg', { msg: error.message }) : '启动升级失败: ' + error.message, 'error'); + document.getElementById('ota-state-text').textContent = typeof t === 'function' ? t('otaPage.stateError') : '错误'; document.getElementById('ota-message').textContent = error.message || '网络错误'; document.getElementById('ota-abort-btn').style.display = 'none'; } @@ -15439,7 +15506,7 @@ async function otaFromUrl() { async function otaFromFile() { const filepath = document.getElementById('ota-file-input').value.trim(); if (!filepath) { - showToast('请输入文件路径', 'error'); + showToast(typeof t === 'function' ? t('toast.enterFilePath') : '请输入文件路径', 'error'); return; } @@ -15459,41 +15526,41 @@ async function otaFromFile() { const progressSection = document.getElementById('ota-progress-section'); progressSection.style.display = 'block'; const stepText = includeWww ? '[1/2] ' : ''; - document.getElementById('ota-state-text').textContent = stepText + '正在读取文件...'; + document.getElementById('ota-state-text').textContent = stepText + (typeof t === 'function' ? t('otaPage.readingFile') : '正在读取文件...'); document.getElementById('ota-progress-bar').style.width = '0%'; document.getElementById('ota-progress-percent').textContent = '0%'; - document.getElementById('ota-progress-size').textContent = '准备中...'; + document.getElementById('ota-progress-size').textContent = typeof t === 'function' ? t('otaPage.preparing') : '准备中...'; document.getElementById('ota-message').textContent = filepath; document.getElementById('ota-abort-btn').style.display = 'inline-block'; try { - showToast('开始从文件升级固件...', 'info'); + showToast(typeof t === 'function' ? t('toast.startingFileUpgrade') : '开始从文件升级固件...', 'info'); const result = await api.call('ota.upgrade_file', params); if (result.code === 0) { - showToast('固件升级已启动', 'success'); - document.getElementById('ota-state-text').textContent = '写入中...'; + showToast(typeof t === 'function' ? t('toast.firmwareUpgradeStarted') : '固件升级已启动', 'success'); + document.getElementById('ota-state-text').textContent = typeof t === 'function' ? t('otaPage.writing') : '写入中...'; // 开始刷新进度 if (!refreshInterval) { refreshInterval = setInterval(refreshOtaProgress, 1000); } await refreshOtaProgress(); } else { - showToast('启动升级失败: ' + result.message, 'error'); - document.getElementById('ota-state-text').textContent = '错误'; + showToast(typeof t === 'function' ? t('toast.upgradeStartFailedMsg', { msg: result.message }) : '启动升级失败: ' + result.message, 'error'); + document.getElementById('ota-state-text').textContent = typeof t === 'function' ? t('otaPage.stateError') : '错误'; document.getElementById('ota-message').textContent = result.message || '启动失败'; document.getElementById('ota-abort-btn').style.display = 'none'; } } catch (error) { - showToast('启动升级失败: ' + error.message, 'error'); - document.getElementById('ota-state-text').textContent = '错误'; + showToast(typeof t === 'function' ? t('toast.upgradeStartFailedMsg', { msg: error.message }) : '启动升级失败: ' + error.message, 'error'); + document.getElementById('ota-state-text').textContent = typeof t === 'function' ? t('otaPage.stateError') : '错误'; document.getElementById('ota-message').textContent = error.message || '网络错误'; document.getElementById('ota-abort-btn').style.display = 'none'; } } async function validateOta() { - if (!confirm('确认将当前固件标记为有效?\n这将取消自动回滚保护。')) { + if (!confirm(typeof t === 'function' ? t('automation.confirmMarkFirmwareValid') : '确认将当前固件标记为有效?\n这将取消自动回滚保护。')) { return; } @@ -15501,18 +15568,18 @@ async function validateOta() { const result = await api.call('ota.validate'); if (result.code === 0) { - showToast('固件已标记为有效', 'success'); + showToast(typeof t === 'function' ? t('toast.firmwareValidated') : '固件已标记为有效', 'success'); await refreshOtaInfo(); } else { - showToast('操作失败: ' + result.message, 'error'); + showToast(typeof t === 'function' ? t('toast.operationFailedMsg', { msg: result.message }) : '操作失败: ' + result.message, 'error'); } } catch (error) { - showToast('操作失败: ' + error.message, 'error'); + showToast(typeof t === 'function' ? t('toast.operationFailedMsg', { msg: error.message }) : '操作失败: ' + error.message, 'error'); } } function confirmRollback() { - if (!confirm('确认回滚到上一版本固件?\n\n系统将立即重启并加载上一个分区的固件。\n请确保上一版本固件可用!')) { + if (!confirm(typeof t === 'function' ? t('automation.confirmRollback') : '确认回滚到上一版本固件?\n\n系统将立即重启并加载上一个分区的固件。\n请确保上一版本固件可用!')) { return; } @@ -15521,22 +15588,22 @@ function confirmRollback() { async function rollbackOta() { try { - showToast('正在回滚固件...', 'info'); + showToast(typeof t === 'function' ? t('toast.rollingBack') : '正在回滚固件...', 'info'); const result = await api.call('ota.rollback'); if (result.code === 0) { - showToast('回滚成功!系统将在 3 秒后重启...', 'success'); + showToast(typeof t === 'function' ? t('toast.rollbackSuccess') : '回滚成功!系统将在 3 秒后重启...', 'success'); // 3秒后页面会因为重启而断开连接 } else { - showToast('回滚失败: ' + result.message, 'error'); + showToast(typeof t === 'function' ? t('toast.rollbackFailedMsg', { msg: result.message }) : '回滚失败: ' + result.message, 'error'); } } catch (error) { - showToast('回滚失败: ' + error.message, 'error'); + showToast(typeof t === 'function' ? t('toast.rollbackFailedMsg', { msg: error.message }) : '回滚失败: ' + error.message, 'error'); } } async function abortOta() { - if (!confirm('确认中止当前升级?')) { + if (!confirm(typeof t === 'function' ? t('automation.confirmAbortUpgrade') : '确认中止当前升级?')) { return; } @@ -15550,16 +15617,16 @@ async function abortOta() { } if (result.code === 0) { - showToast('升级已中止', 'info'); + showToast(typeof t === 'function' ? t('toast.upgradeAborted') : '升级已中止', 'info'); otaStep = 'idle'; await refreshOtaInfo(); clearInterval(refreshInterval); refreshInterval = null; } else { - showToast('中止失败: ' + result.message, 'error'); + showToast(typeof t === 'function' ? t('toast.abortFailedMsg', { msg: result.message }) : '中止失败: ' + result.message, 'error'); } } catch (error) { - showToast('中止失败: ' + error.message, 'error'); + showToast(typeof t === 'function' ? t('toast.abortFailedMsg', { msg: error.message }) : '中止失败: ' + error.message, 'error'); } } @@ -15668,15 +15735,15 @@ async function saveOtaServer() { if (result.code === 0) { if (serverUrl) { - showToast('OTA 服务器地址已保存', 'success'); + showToast(typeof t === 'function' ? t('toast.otaServerSaved') : 'OTA 服务器地址已保存', 'success'); } else { - showToast('OTA 服务器地址已清除', 'info'); + showToast(typeof t === 'function' ? t('toast.otaServerCleared') : 'OTA 服务器地址已清除', 'info'); } } else { - showToast('保存失败: ' + result.message, 'error'); + showToast(typeof t === 'function' ? t('toast.saveFailedMsg', { msg: result.message }) : '保存失败: ' + result.message, 'error'); } } catch (error) { - showToast('保存失败: ' + error.message, 'error'); + showToast(typeof t === 'function' ? t('toast.saveFailedMsg', { msg: error.message }) : '保存失败: ' + error.message, 'error'); } } @@ -15686,7 +15753,7 @@ let currentFirmwareVersion = null; async function checkForUpdates() { const serverUrl = document.getElementById('ota-server-input').value.trim(); if (!serverUrl) { - showToast('请先输入 OTA 服务器地址', 'error'); + showToast(typeof t === 'function' ? t('toast.enterOtaServer') : '请先输入 OTA 服务器地址', 'error'); return; } @@ -15761,11 +15828,11 @@ async function checkForUpdates() { const localParts = parseVersion(localVersion); const serverParts = parseVersion(serverVersion); if (serverParts.major > localParts.major) { - updateType = '主版本更新'; + updateType = '' + (typeof t === 'function' ? t('otaPage.majorUpdate') : '主版本更新') + ''; } else if (serverParts.minor > localParts.minor) { - updateType = '功能更新'; + updateType = '' + (typeof t === 'function' ? t('otaPage.featureUpdate') : '功能更新') + ''; } else { - updateType = '补丁更新'; + updateType = '' + (typeof t === 'function' ? t('otaPage.patchUpdate') : '补丁更新') + ''; } } @@ -15774,7 +15841,7 @@ async function checkForUpdates() { statusDiv.innerHTML = `
- 发现新版本 + ${typeof t === 'function' ? t('otaPage.newVersionFound') : '发现新版本'} ${updateType ? ` · ${updateType}` : ''}
${localVersion}${serverVersion} @@ -15791,7 +15858,7 @@ async function checkForUpdates() { statusDiv.innerHTML = `
- 服务器版本较旧 + ${typeof t === 'function' ? t('otaPage.serverVersionOlder') : '服务器版本较旧'}
${localVersion}${serverVersion}
@@ -15805,7 +15872,7 @@ async function checkForUpdates() { statusDiv.className = 'ota-update-status no-update'; statusDiv.innerHTML = `
- 已是最新版本 + ${typeof t === 'function' ? t('otaPage.alreadyLatest') : '已是最新版本'} ${localVersion}
`; @@ -15816,7 +15883,7 @@ async function checkForUpdates() { statusDiv.className = 'ota-update-status error'; statusDiv.innerHTML = `
- 检查更新失败 + ${typeof t === 'function' ? t('otaPage.checkUpdateFailed') : '检查更新失败'}
${error.message}
`; @@ -15826,7 +15893,7 @@ async function checkForUpdates() { async function upgradeFromServer() { const serverUrl = document.getElementById('ota-server-input').value.trim(); if (!serverUrl) { - showToast('OTA 服务器地址未设置', 'error'); + showToast(typeof t === 'function' ? t('toast.otaServerNotSet') : 'OTA 服务器地址未设置', 'error'); return; } @@ -15836,7 +15903,7 @@ async function upgradeFromServer() { document.getElementById('ota-state-text').textContent = typeof t === 'function' ? t('otaPage.preparingUpgrade') : 'Preparing upgrade...'; document.getElementById('ota-progress-bar').style.width = '0%'; document.getElementById('ota-progress-percent').textContent = ''; - document.getElementById('ota-progress-size').textContent = '正在初始化...'; + document.getElementById('ota-progress-size').textContent = typeof t === 'function' ? t('otaPage.initializing') : '正在初始化...'; document.getElementById('ota-message').textContent = serverUrl; document.getElementById('ota-abort-btn').style.display = 'none'; @@ -15883,10 +15950,10 @@ async function upgradeViaProxy(serverUrl) { // ===== 第一步:浏览器下载固件 ===== updateStep(1, '下载固件中...'); const firmwareUrl = serverUrl.replace(/\/$/, '') + '/firmware'; - messageEl.textContent = '从 OTA 服务器下载'; + messageEl.textContent = typeof t === 'function' ? t('otaPage.downloadingFromServer') : '从 OTA 服务器下载'; progressBar.style.width = '0%'; progressPercent.textContent = '0%'; - progressSize.textContent = '正在连接服务器...'; + progressSize.textContent = typeof t === 'function' ? t('otaPage.connectingServer') : '正在连接服务器...'; abortBtn.style.display = 'none'; // 浏览器下载阶段暂不支持中止 console.log('Proxy OTA: Downloading firmware from', firmwareUrl); @@ -15900,14 +15967,14 @@ async function upgradeViaProxy(serverUrl) { }); console.log('Proxy OTA: Firmware downloaded,', firmwareData.byteLength, 'bytes'); - showToast(`固件下载完成 (${formatSize(firmwareData.byteLength)})`, 'success'); + showToast(typeof t === 'function' ? t('ota.firmwareDownloadComplete', { size: formatSize(firmwareData.byteLength) }) : `固件下载完成 (${formatSize(firmwareData.byteLength)})`, 'success'); // ===== 第二步:上传固件到 ESP32 ===== updateStep(2, '上传固件到设备...'); - messageEl.textContent = `固件大小: ${formatSize(firmwareData.byteLength)}`; + messageEl.textContent = typeof t === 'function' ? t('ui.firmwareSize') + ': ' + formatSize(firmwareData.byteLength) : `固件大小: ${formatSize(firmwareData.byteLength)}`; progressBar.style.width = '0%'; progressPercent.textContent = ''; - progressSize.textContent = '正在写入 Flash(这可能需要1-2分钟)...'; + progressSize.textContent = typeof t === 'function' ? t('otaPage.writingFlash') : '正在写入 Flash(这可能需要1-2分钟)...'; // 调用 ESP32 上传接口(复用现有的 /api/v1/ota/firmware) // 注意:不自动重启,等 www 也完成后再重启 @@ -15918,7 +15985,7 @@ async function upgradeViaProxy(serverUrl) { } console.log('Proxy OTA: Firmware uploaded to device'); - showToast('固件写入完成!', 'success'); + showToast(typeof t === 'function' ? t('toast.firmwareWriteComplete') : '固件写入完成!', 'success'); progressBar.style.width = '100%'; progressPercent.textContent = ''; @@ -15926,10 +15993,10 @@ async function upgradeViaProxy(serverUrl) { if (includeWww) { updateStep(3, '下载 WebUI...'); const wwwUrl = serverUrl.replace(/\/$/, '') + '/www'; - messageEl.textContent = '从 OTA 服务器下载'; + messageEl.textContent = typeof t === 'function' ? t('otaPage.downloadingFromServer') : '从 OTA 服务器下载'; progressBar.style.width = '0%'; progressPercent.textContent = '0%'; - progressSize.textContent = '正在连接...'; + progressSize.textContent = typeof t === 'function' ? t('otaPage.connecting') : '正在连接...'; try { // 下载 www.bin @@ -15941,34 +16008,34 @@ async function upgradeViaProxy(serverUrl) { }); console.log('Proxy OTA: WWW downloaded,', wwwData.byteLength, 'bytes'); - showToast(`WebUI 下载完成 (${formatSize(wwwData.byteLength)})`, 'success'); + showToast(typeof t === 'function' ? t('ota.webuiDownloadComplete', { size: formatSize(wwwData.byteLength) }) : `WebUI 下载完成 (${formatSize(wwwData.byteLength)})`, 'success'); // 上传 www.bin updateStep(4, '上传 WebUI 到设备...'); - messageEl.textContent = `WebUI 大小: ${formatSize(wwwData.byteLength)}`; + messageEl.textContent = typeof t === 'function' ? t('ui.webuiSize') + ': ' + formatSize(wwwData.byteLength) : `WebUI 大小: ${formatSize(wwwData.byteLength)}`; progressBar.style.width = '0%'; progressPercent.textContent = ''; - progressSize.textContent = '正在写入 SPIFFS...'; + progressSize.textContent = typeof t === 'function' ? t('otaPage.writingSpiffs') : '正在写入 SPIFFS...'; const wwwResult = await uploadWwwToDevice(wwwData); if (!wwwResult.success) { console.warn('WWW upload failed:', wwwResult.error); - showToast('WebUI 升级跳过: ' + wwwResult.error, 'warning'); + showToast(typeof t === 'function' ? t('toast.webuiUpgradeSkipped', { msg: wwwResult.error }) : 'WebUI 升级跳过: ' + wwwResult.error, 'warning'); } else { console.log('Proxy OTA: WWW uploaded to device'); - showToast('WebUI 写入完成!', 'success'); + showToast(typeof t === 'function' ? t('toast.webuiWriteComplete') : 'WebUI 写入完成!', 'success'); progressBar.style.width = '100%'; progressPercent.textContent = ''; } } catch (wwwError) { console.warn('WWW download/upload failed:', wwwError); - showToast('WebUI 升级跳过: ' + wwwError.message, 'warning'); + showToast(typeof t === 'function' ? t('toast.webuiUpgradeSkipped', { msg: wwwError.message }) : 'WebUI 升级跳过: ' + wwwError.message, 'warning'); } } // ===== 最终步骤:升级完成,触发重启 ===== - stateEl.textContent = '全部升级完成'; + stateEl.textContent = typeof t === 'function' ? t('otaPage.allUpgradeComplete') : '全部升级完成'; progressBar.style.width = '100%'; progressBar.style.background = 'linear-gradient(90deg, #059669, #10b981)'; progressPercent.textContent = ''; @@ -15993,12 +16060,12 @@ async function upgradeViaProxy(serverUrl) { } catch (error) { console.error('Proxy OTA failed:', error); - stateEl.textContent = '升级失败'; + stateEl.textContent = typeof t === 'function' ? t('otaPage.upgradeFailed') : '升级失败'; messageEl.textContent = error.message; progressBar.style.width = '0%'; progressPercent.textContent = ''; otaStep = 'idle'; - showToast('升级失败: ' + error.message, 'error'); + showToast(typeof t === 'function' ? t('toast.upgradeFailedMsg', { msg: error.message }) : '升级失败: ' + error.message, 'error'); } } @@ -16822,12 +16889,12 @@ function formatUptimeSec(seconds) { async function automationControl(action) { try { const result = await api.call(`automation.${action}`); - showToast(`${action}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); + showToast(typeof t === 'function' ? t('toast.actionResult', { action, msg: result.message || 'OK' }) : `${action}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); if (result.code === 0) { await refreshAutomationStatus(); } } catch (error) { - showToast(`${action} 失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.actionFailedMsg', { action, msg: error.message }) : `${action} 失败: ${error.message}`, 'error'); } } @@ -16886,11 +16953,11 @@ async function refreshRules() { ${r.actions_count || 0} ${r.trigger_count || 0} - - - - - + + + + + `}).join('')} @@ -16913,12 +16980,12 @@ async function toggleRule(id, enable) { try { const action = enable ? 'automation.rules.enable' : 'automation.rules.disable'; const result = await api.call(action, { id }); - showToast(`规则 ${id} ${enable ? '启用' : '禁用'}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); + showToast(typeof t === 'function' ? t('toast.ruleToggled', { id, state: enable ? t('status.enabled') : t('status.disabled') }) + ': ' + (result.message || 'OK') : `规则 ${id} ${enable ? '启用' : '禁用'}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); if (result.code === 0) { await refreshRules(); } } catch (error) { - showToast(`切换规则状态失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.toggleRuleFailed') + ': ' + error.message : `切换规则状态失败: ${error.message}`, 'error'); } } @@ -16928,9 +16995,9 @@ async function toggleRule(id, enable) { async function triggerRule(id) { try { const result = await api.call('automation.rules.trigger', { id }); - showToast(`触发规则 ${id}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); + showToast(typeof t === 'function' ? t('toast.ruleTriggered', { id }) + ': ' + (result.message || 'OK') : `触发规则 ${id}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); } catch (error) { - showToast(`触发规则失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.triggerRuleFailed') + ': ' + error.message : `触发规则失败: ${error.message}`, 'error'); } } @@ -16974,10 +17041,10 @@ async function refreshSources() { ${s.enabled ? enStr : disStr} ${s.poll_interval_ms ? (s.poll_interval_ms / 1000) + secStr : '-'} - - - - + + + + `).join('')} @@ -17013,7 +17080,7 @@ async function refreshVariables() { if (countBadge) countBadge.textContent = allVariables.length; renderVariables(allVariables); } else { - container.innerHTML = `

${result.message || '获取变量失败'}

`; + container.innerHTML = `

${result.message || (typeof t === 'function' ? t('sshPage.getVarFailed') : '获取变量失败')}

`; } } catch (error) { container.innerHTML = `

${error.message}

`; @@ -17044,7 +17111,7 @@ function renderVariables(variables) { if (!container) return; if (variables.length === 0) { - container.innerHTML = '

暂无变量数据

'; + container.innerHTML = '

' + (typeof t === 'function' ? t('automationPage.noVariables') : '暂无变量数据') + '

'; return; } @@ -17068,10 +17135,10 @@ function renderVariables(variables) { - - - - + + + + @@ -17184,10 +17251,10 @@ async function refreshActions() { `).join('')} @@ -17457,11 +17524,11 @@ function updateActionTypeFields() { - - - - - + + + + + @@ -17473,11 +17540,11 @@ function updateActionTypeFields() {
- 日志配置 + ${typeof t === 'function' ? t('automation.logConfig') : '日志配置'}
- + - 支持变量: \${变量名} + + + ${typeof t === 'function' ? t('automation.logMsgHint') : '支持变量: ${变量名}'}
`, @@ -17497,15 +17564,15 @@ function updateActionTypeFields() {
- 变量配置 + ${typeof t === 'function' ? t('automation.varConfig') : '变量配置'}
- +
- - + + 示例: true, 123, \${other_var}
@@ -17522,7 +17589,7 @@ function updateActionTypeFields() {
- + JSON 格式,支持变量
@@ -17573,7 +17640,7 @@ async function submitAction() { const async = document.getElementById('action-async')?.checked || false; if (!id) { - showToast('请填写动作 ID', 'error'); + showToast(typeof t === 'function' ? t('toast.fillActionId') : '请填写动作 ID', 'error'); return; } if (!type) { @@ -17588,7 +17655,7 @@ async function submitAction() { case 'cli': const cliCmd = document.getElementById('action-cli-command')?.value?.trim(); if (!cliCmd) { - showToast('请填写命令行', 'error'); + showToast(typeof t === 'function' ? t('toast.fillCommand') : '请填写命令行', 'error'); return; } data.cli = { @@ -17600,7 +17667,7 @@ async function submitAction() { case 'ssh_cmd_ref': const cmdId = document.getElementById('action-ssh-cmd-id')?.value; if (!cmdId) { - showToast('请选择 SSH 命令', 'error'); + showToast(typeof t === 'function' ? t('toast.selectSshCommand') : '请选择 SSH 命令', 'error'); return; } data.ssh_ref = { cmd_id: cmdId }; @@ -17608,7 +17675,7 @@ async function submitAction() { case 'led': const ledDevice = document.getElementById('action-led-device')?.value; if (!ledDevice) { - showToast('请选择 LED 设备', 'error'); + showToast(typeof t === 'function' ? t('toast.selectLedDevice') : '请选择 LED 设备', 'error'); return; } const isMatrix = ledDevice === 'matrix'; @@ -17634,7 +17701,7 @@ async function submitAction() { data.led.speed = parseInt(document.getElementById('action-led-speed')?.value) || 50; data.led.color = document.getElementById('action-led-color')?.value || '#FF0000'; if (!data.led.effect) { - showToast('请选择动画', 'error'); + showToast(typeof t === 'function' ? t('toast.selectAnimation') : '请选择动画', 'error'); return; } break; @@ -17647,7 +17714,7 @@ async function submitAction() { case 'text': data.led.text = document.getElementById('action-led-text')?.value?.trim(); if (!data.led.text) { - showToast('请输入文本内容', 'error'); + showToast(typeof t === 'function' ? t('toast.enterText') : '请输入文本内容', 'error'); return; } data.led.font = document.getElementById('action-led-font')?.value || ''; @@ -17663,7 +17730,7 @@ async function submitAction() { case 'image': data.led.image_path = document.getElementById('action-led-image-path')?.value?.trim(); if (!data.led.image_path) { - showToast('请输入图像路径', 'error'); + showToast((typeof t === 'function' ? t('toast.enterImagePath') : '请输入图像路径'), 'error'); return; } data.led.center = document.getElementById('action-led-center')?.checked || false; @@ -17671,7 +17738,7 @@ async function submitAction() { case 'qrcode': data.led.qr_text = document.getElementById('action-led-qr-text')?.value?.trim(); if (!data.led.qr_text) { - showToast('请输入QR码内容', 'error'); + showToast(typeof t === 'function' ? t('toast.enterQrContent') : '请输入QR码内容', 'error'); return; } data.led.qr_ecc = document.getElementById('action-led-qr-ecc')?.value || 'M'; @@ -17681,7 +17748,7 @@ async function submitAction() { case 'filter': data.led.filter = document.getElementById('action-led-filter')?.value; if (!data.led.filter) { - showToast('请选择滤镜', 'error'); + showToast(typeof t === 'function' ? t('toast.selectFilter') : '请选择滤镜', 'error'); return; } // 根据滤镜类型收集对应参数 @@ -17701,7 +17768,7 @@ async function submitAction() { case 'log': const logMsg = document.getElementById('action-log-message')?.value?.trim(); if (!logMsg) { - showToast('请填写日志消息', 'error'); + showToast(typeof t === 'function' ? t('toast.fillLogMessage') : '请填写日志消息', 'error'); return; } data.log = { @@ -17713,7 +17780,7 @@ async function submitAction() { const varName = document.getElementById('action-var-name')?.value?.trim(); const varValue = document.getElementById('action-var-value')?.value?.trim(); if (!varName || !varValue) { - showToast('请填写变量名和值', 'error'); + showToast(typeof t === 'function' ? t('toast.fillVarNameValue') : '请填写变量名和值', 'error'); return; } data.set_var = { @@ -17724,7 +17791,7 @@ async function submitAction() { case 'webhook': const webhookUrl = document.getElementById('action-webhook-url')?.value?.trim(); if (!webhookUrl) { - showToast('请填写 Webhook URL', 'error'); + showToast(typeof t === 'function' ? t('toast.fillWebhookUrl') : '请填写 Webhook URL', 'error'); return; } data.webhook = { @@ -17738,14 +17805,14 @@ async function submitAction() { try { const result = await api.call('automation.actions.add', data); if (result.code === 0) { - showToast(`动作模板 ${id} 创建成功`, 'success'); + showToast(typeof t === 'function' ? t('toast.actionCreated', { id }) : `动作模板 ${id} 创建成功`, 'success'); closeModal('action-modal'); await refreshActions(); } else { - showToast(`创建失败: ${result.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.actionCreateFailed') + ': ' + result.message : `创建失败: ${result.message}`, 'error'); } } catch (error) { - showToast(`创建失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.actionCreateFailed') + ': ' + error.message : `创建失败: ${error.message}`, 'error'); } } @@ -17788,7 +17855,7 @@ async function loadSshHostsForAction() { console.error('加载 SSH 主机列表失败:', e); const select = document.getElementById('action-ssh-host'); if (select) { - select.innerHTML = ''; + select.innerHTML = ''; } } } @@ -17941,7 +18008,7 @@ function updateActionLedTypeFields() { case 'fill': html = `
- +
@@ -17957,15 +18024,15 @@ function updateActionLedTypeFields() {
- +
128
- - + +
`; @@ -17975,21 +18042,21 @@ function updateActionLedTypeFields() { const effectOptions = effects.map(e => ``).join(''); html = `
- +
- +
50
- +
@@ -17999,7 +18066,7 @@ function updateActionLedTypeFields() { case 'brightness': html = `
- +
128 @@ -18009,31 +18076,31 @@ function updateActionLedTypeFields() { break; case 'off': - html = `
关闭 LED 设备,无需额外参数
`; + html = `
${typeof t === 'function' ? t('automation.ledOffHint') : '关闭 LED 设备,无需额外参数'}
`; break; case 'filter_stop': - html = `
停止当前运行的滤镜效果,无需额外参数
`; + html = `
${typeof t === 'function' ? t('automation.filterStopHint') : '停止当前运行的滤镜效果,无需额外参数'}
`; break; case 'text_stop': - html = `
停止当前运行的文本覆盖层,无需额外参数
`; + html = `
${typeof t === 'function' ? t('automation.textStopHint') : '停止当前运行的文本覆盖层,无需额外参数'}
`; break; case 'text': html = `
- +
- - + +
- +
@@ -18043,21 +18110,21 @@ function updateActionLedTypeFields() {
- +
- +
@@ -18072,17 +18139,17 @@ function updateActionLedTypeFields() {
- +
- +
- +
`; @@ -18093,16 +18160,16 @@ function updateActionLedTypeFields() { case 'image': html = `
- +
- + - +
- 支持 PNG、JPG、BMP、GIF 格式,路径支持变量 + ${typeof t === 'function' ? t('automation.imagePathHint') : '支持 PNG、JPG、BMP、GIF 格式,路径支持变量'}
- +
`; break; @@ -18190,7 +18257,7 @@ async function loadActionLedFonts() { return fontExts.includes(ext); }); - fontSelect.innerHTML = ''; + fontSelect.innerHTML = ''; fonts.forEach(f => { const option = document.createElement('option'); const baseName = f.name.substring(0, f.name.lastIndexOf('.')); @@ -18239,7 +18306,7 @@ async function showImageSelectModal(title, onSelect) {
@@ -18555,7 +18622,7 @@ function selectVariable(varName) { saveDataWidgets(); renderDataWidgets(); refreshDataWidgets(); - showToast(`已绑定 ${widget.label} → ${varName}`, 'success'); + showToast(typeof t === 'function' ? t('toast.widgetBound', { widget: widget.label, var: varName }) : `已绑定 ${widget.label} → ${varName}`, 'success'); } } closeModal('variable-select-modal'); @@ -18650,7 +18717,7 @@ function updateActionFilterParams() { */ async function testAction(id) { try { - showToast(`正在执行动作: ${id}...`, 'info'); + showToast(typeof t === 'function' ? t('toast.actionExecuting', { id }) : `正在执行动作: ${id}...`, 'info'); const result = await api.call('automation.actions.execute', { id }); console.log('Action execute result:', result); @@ -18661,11 +18728,11 @@ async function testAction(id) { } showToast(msg, 'success'); } else { - showToast(`动作 ${id} 失败: ${result.message || '未知错误'}`, 'error'); + showToast(typeof t === 'function' ? t('toast.actionFailed', { id }) + ': ' + (result.message || t('common.unknown')) : `动作 ${id} 失败: ${result.message || '未知错误'}`, 'error'); } } catch (error) { console.error('Action execute error:', error); - showToast(`动作执行失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.actionExecuteFailed') + ': ' + error.message : `动作执行失败: ${error.message}`, 'error'); } } @@ -18676,7 +18743,7 @@ async function editAction(id) { try { const result = await api.call('automation.actions.get', { id }); if (result.code !== 0) { - showToast(`获取动作详情失败: ${result.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.getActionFailed') + ': ' + result.message : `获取动作详情失败: ${result.message}`, 'error'); return; } @@ -18883,16 +18950,16 @@ async function editAction(id) { // 更改模态框标题和按钮 const modalTitle = document.querySelector('#action-modal .modal-header h3'); - if (modalTitle) modalTitle.textContent = '编辑动作模板'; + if (modalTitle) modalTitle.textContent = typeof t === 'function' ? t('otaPage.editActionTemplate') : '编辑动作模板'; const submitBtn = document.querySelector('#action-modal button[onclick="submitAction()"]'); if (submitBtn) { - submitBtn.textContent = '更新'; + submitBtn.textContent = typeof t === 'function' ? t('otaPage.updateAction') : '更新'; submitBtn.setAttribute('onclick', `updateAction('${tpl.id}')`); } } catch (error) { - showToast(`编辑动作失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.editActionFailed') + ': ' + error.message : `编辑动作失败: ${error.message}`, 'error'); } } @@ -18908,7 +18975,7 @@ async function updateAction(originalId) { // 删除旧模板 const deleteResult = await api.call('automation.actions.delete', { id: originalId }); if (deleteResult.code !== 0) { - showToast(`更新失败: 无法删除旧模板`, 'error'); + showToast(typeof t === 'function' ? t('toast.deleteOldTemplateFailed') : '更新失败: 无法删除旧模板', 'error'); return; } @@ -18919,7 +18986,7 @@ async function updateAction(originalId) { await submitAction(); } catch (error) { - showToast(`更新失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.updateFailed') + ': ' + error.message : `更新失败: ${error.message}`, 'error'); } } @@ -18927,16 +18994,16 @@ async function updateAction(originalId) { * 删除动作 */ async function deleteAction(id) { - if (!confirm(`确定要删除动作模板 "${id}" 吗?`)) return; + if (!confirm(typeof t === 'function' ? t('ui.confirmDeleteAction', { id }) : `确定要删除动作模板 "${id}" 吗?`)) return; try { const result = await api.call('automation.actions.delete', { id }); - showToast(`删除动作 ${id}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); + showToast(typeof t === 'function' ? t('toast.deleteActionResult', { id }) + ': ' + (result.message || 'OK') : `删除动作 ${id}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); if (result.code === 0) { await refreshActions(); } } catch (error) { - showToast(`删除失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.deleteFailedMsg', { msg: error.message }) : `删除失败: ${error.message}`, 'error'); } } @@ -18947,12 +19014,12 @@ async function toggleSource(id, enable) { try { const action = enable ? 'automation.sources.enable' : 'automation.sources.disable'; const result = await api.call(action, { id }); - showToast(`数据源 ${id} ${enable ? '启用' : '禁用'}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); + showToast(typeof t === 'function' ? t('toast.sourceToggled', { id, state: enable ? t('status.enabled') : t('status.disabled') }) + ': ' + (result.message || 'OK') : `数据源 ${id} ${enable ? '启用' : '禁用'}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); if (result.code === 0) { await refreshSources(); } } catch (error) { - showToast(`切换数据源状态失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.toggleSourceFailed') + ': ' + error.message : `切换数据源状态失败: ${error.message}`, 'error'); } } @@ -18960,18 +19027,18 @@ async function toggleSource(id, enable) { * 删除数据源 */ async function deleteSource(id) { - if (!confirm(`确定要删除数据源 "${id}" 吗?此操作不可撤销。`)) { + if (!confirm(typeof t === 'function' ? t('ui.confirmDeleteSource', { id }) : `确定要删除数据源 "${id}" 吗?此操作不可撤销。`)) { return; } try { const result = await api.call('automation.sources.delete', { id }); - showToast(`删除数据源 ${id}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); + showToast(typeof t === 'function' ? t('toast.deleteSourceResult', { id }) + ': ' + (result.message || 'OK') : `删除数据源 ${id}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); if (result.code === 0) { await Promise.all([refreshSources(), refreshAutomationStatus()]); } } catch (error) { - showToast(`删除数据源失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.deleteSourceFailed') + ': ' + error.message : `删除数据源失败: ${error.message}`, 'error'); } } @@ -18985,7 +19052,7 @@ async function showSourceVariables(sourceId) { // 更新标题 const header = modal.querySelector('.modal-header h2'); - if (header) header.textContent = `${sourceId} 变量`; + if (header) header.textContent = typeof t === 'function' ? t('ui.sourceVariables', { source: sourceId }) : `${sourceId} 变量`; body.innerHTML = '
' + t('common.loading') + '
'; modal.classList.remove('hidden'); @@ -18997,7 +19064,7 @@ async function showSourceVariables(sourceId) { const vars = result.data.variables.filter(v => v.source_id === sourceId); if (vars.length === 0) { - body.innerHTML = '

该数据源暂无变量数据

'; + body.innerHTML = '

' + (typeof t === 'function' ? t('automationPage.sourceNoData') : '该数据源暂无变量数据') + '

'; return; } @@ -19005,10 +19072,10 @@ async function showSourceVariables(sourceId) {
变量名类型当前值更新时间${typeof t === 'function' ? t('sshPage.varTableName') : '变量名'}${typeof t === 'function' ? t('sshPage.varTableType') : '类型'}${typeof t === 'function' ? t('sshPage.varTableValue') : '当前值'}${typeof t === 'function' ? t('sshPage.varTableUpdated') : '更新时间'}
${a.async ? '' + asyncStr + '' : '' + syncStr + ''} ${a.description || '-'} - - - - + + + +
- - - - + + + + @@ -19024,7 +19091,7 @@ async function showSourceVariables(sourceId) {
变量名类型当前值更新时间${typeof t === 'function' ? t('sshPage.varTableName') : '变量名'}${typeof t === 'function' ? t('sshPage.varTableType') : '类型'}${typeof t === 'function' ? t('sshPage.varTableValue') : '当前值'}${typeof t === 'function' ? t('sshPage.varTableUpdated') : '更新时间'}
`; } else { - body.innerHTML = `

${result.message || '获取变量失败'}

`; + body.innerHTML = `

${result.message || (typeof t === 'function' ? t('sshPage.getVarFailed') : '获取变量失败')}

`; } } catch (error) { body.innerHTML = `

${error.message}

`; @@ -19104,13 +19171,13 @@ async function saveShutdownSettings() { // 验证 if (config.low_threshold >= config.recovery_threshold) { - errorDiv.textContent = '低电压阈值必须小于恢复电压阈值'; + errorDiv.textContent = typeof t === 'function' ? t('otaPage.lowVoltageError') : '低电压阈值必须小于恢复电压阈值'; errorDiv.classList.remove('hidden'); return; } if (config.shutdown_delay < 10 || config.shutdown_delay > 600) { - errorDiv.textContent = '关机倒计时必须在 10-600 秒之间'; + errorDiv.textContent = typeof t === 'function' ? t('otaPage.shutdownDelayError') : '关机倒计时必须在 10-600 秒之间'; errorDiv.classList.remove('hidden'); return; } @@ -19118,14 +19185,14 @@ async function saveShutdownSettings() { try { const result = await api.powerProtectionSet(config); if (result.code === 0) { - showToast('关机设置已保存', 'success'); + showToast(typeof t === 'function' ? t('toast.shutdownSettingsSaved') : '关机设置已保存', 'success'); closeShutdownSettingsModal(); } else { errorDiv.textContent = result.message || '保存失败'; errorDiv.classList.remove('hidden'); } } catch (e) { - errorDiv.textContent = '保存失败: ' + e.message; + errorDiv.textContent = typeof t === 'function' ? t('toast.saveFailedMsg', { msg: e.message }) : '保存失败: ' + e.message; errorDiv.classList.remove('hidden'); } } @@ -19134,7 +19201,7 @@ async function saveShutdownSettings() { * 恢复默认关机设置 */ async function resetShutdownSettings() { - if (!confirm('确认恢复默认设置?')) return; + if (!confirm(typeof t === 'function' ? t('automation.confirmRestoreDefaults') : '确认恢复默认设置?')) return; const config = { low_threshold: 12.6, @@ -19154,12 +19221,12 @@ async function resetShutdownSettings() { document.getElementById('ss-shutdown-delay').value = 60; document.getElementById('ss-recovery-hold').value = 5; document.getElementById('ss-fan-stop-delay').value = 60; - showToast('已恢复默认设置', 'success'); + showToast(typeof t === 'function' ? t('toast.defaultsRestored') : '已恢复默认设置', 'success'); } else { - showToast('恢复失败: ' + (result.message || 'Unknown error'), 'error'); + showToast(typeof t === 'function' ? t('toast.restoreFailedMsg', { msg: result.message || t('common.unknown') }) : '恢复失败: ' + (result.message || '未知错误'), 'error'); } } catch (e) { - showToast('恢复失败: ' + e.message, 'error'); + showToast(typeof t === 'function' ? t('toast.restoreFailedMsg', { msg: e.message }) : '恢复失败: ' + e.message, 'error'); } } @@ -19167,18 +19234,18 @@ async function resetShutdownSettings() { * 删除规则 */ async function deleteRule(id) { - if (!confirm(`确定要删除规则 "${id}" 吗?此操作不可撤销。`)) { + if (!confirm(typeof t === 'function' ? t('ui.confirmDeleteRule', { id }) : `确定要删除规则 "${id}" 吗?此操作不可撤销。`)) { return; } try { const result = await api.call('automation.rules.delete', { id }); - showToast(`删除规则 ${id}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); + showToast(typeof t === 'function' ? t('toast.deleteRuleResult', { id }) + ': ' + (result.message || 'OK') : `删除规则 ${id}: ${result.message || 'OK'}`, result.code === 0 ? 'success' : 'error'); if (result.code === 0) { await Promise.all([refreshRules(), refreshAutomationStatus()]); } } catch (error) { - showToast(`删除规则失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.deleteRuleFailed') + ': ' + error.message : `删除规则失败: ${error.message}`, 'error'); } } @@ -19190,14 +19257,14 @@ async function editRule(id) { // 获取规则详情 const result = await api.call('automation.rules.get', { id }); if (result.code !== 0 || !result.data) { - showToast(`获取规则详情失败: ${result.message || '未知错误'}`, 'error'); + showToast(typeof t === 'function' ? t('toast.getRuleDetailFailed') + ': ' + (result.message || t('common.unknown')) : `获取规则详情失败: ${result.message || '未知错误'}`, 'error'); return; } // 打开编辑模态框 showAddRuleModal(result.data); } catch (error) { - showToast(`获取规则详情失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.getRuleDetailFailed') + ': ' + error.message : `获取规则详情失败: ${error.message}`, 'error'); } } @@ -19352,7 +19419,7 @@ function showAddSourceModal() { ${typeof t === 'function' ? t('automation.autoDiscoverFields') : '自动发现所有 JSON 字段为变量'} - ${typeof t === 'function' ? t('automation.autoDiscoverHint') : '关闭后仅使用上方选中的字段作为变量'} + ${typeof t === 'function' ? t('automation.autoDiscoverHint') : '关闭后仅使用上方选中的字段作为变量'}
-
- +
+ ${typeof t === 'function' ? t('automation.iconLabel') : '图标'}
@@ -20147,22 +20214,25 @@ function showAddRuleModal(ruleData = null) {
-
-
- - -
-
- - +
+ ${typeof t === 'function' ? (t('automation.ruleOptions') || '规则选项') : '规则选项'} +
+
+ + +
+
+ + +
+
-
@@ -20346,9 +20416,10 @@ async function browseRuleIconImage() { function updateRuleIconPreview(path) { const preview = document.getElementById('rule-icon-preview'); if (path && path.startsWith('/sdcard/')) { - preview.innerHTML = `icon`; + const loadFailed = typeof t === 'function' ? t('sshPage.iconPreviewFailed') : '加载失败'; + preview.innerHTML = `icon`; } else { - preview.innerHTML = ''; + preview.innerHTML = '' + (typeof t === 'function' ? t('sshPage.iconPreviewNone') : '无') + ''; } } @@ -20600,7 +20671,7 @@ async function addActionTemplateRow(templateId = '', delayMs = 0, repeatMode = ' await loadActionTemplatesForRule(); if (cachedActionTemplates.length === 0) { - showToast('请先创建动作模板', 'warning'); + showToast(typeof t === 'function' ? t('toast.createActionFirst') : '请先创建动作模板', 'warning'); return; } @@ -20971,8 +21042,8 @@ function updateActionFields(selectElement) { break; case 'set_var': paramsContainer.innerHTML = ` - - + + `; break; case 'log': @@ -20982,7 +21053,7 @@ function updateActionFields(selectElement) { - + `; break; case 'webhook': @@ -21096,7 +21167,7 @@ async function submitAddRule(originalId = null) { }); if (actions.length === 0) { - alert('请至少选择一个动作模板'); + alert(typeof t === 'function' ? t('automation.alertSelectAction') : '请至少选择一个动作模板'); return; } @@ -21120,14 +21191,14 @@ async function submitAddRule(originalId = null) { const result = await api.call('automation.rules.add', params); if (result.code === 0) { - showToast(`规则 ${id} ${isEdit ? '更新' : '创建'}成功`, 'success'); + showToast(typeof t === 'function' ? t('toast.ruleCreated', { id, action: isEdit ? t('toast.ruleUpdate') : t('toast.ruleCreate') }) : `规则 ${id} ${isEdit ? '更新' : '创建'}成功`, 'success'); closeModal('add-rule-modal'); await Promise.all([refreshRules(), refreshAutomationStatus()]); } else { - showToast(`${isEdit ? '更新' : '创建'}规则失败: ${result.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.ruleCreateFailed', { action: isEdit ? t('toast.ruleUpdate') : t('toast.ruleCreate') }) + ': ' + result.message : `${isEdit ? '更新' : '创建'}规则失败: ${result.message}`, 'error'); } } catch (error) { - showToast(`${isEdit ? '更新' : '创建'}规则失败: ${error.message}`, 'error'); + showToast(typeof t === 'function' ? t('toast.ruleCreateFailed', { action: isEdit ? t('toast.ruleUpdate') : t('toast.ruleCreate') }) + ': ' + error.message : `${isEdit ? '更新' : '创建'}规则失败: ${error.message}`, 'error'); } } @@ -21383,12 +21454,12 @@ async function previewSourceImport() { `; if (data.exists) { - html += `
该配置已存在,导入将覆盖现有文件
`; + html += `
${typeof t === 'function' ? t('securityPage.configExistsWarning') : '该配置已存在,导入将覆盖现有文件'}
`; } previewDiv.innerHTML = html; step2.style.display = 'block'; resultBox.className = 'result-box success'; - resultBox.textContent = '签名验证通过'; + resultBox.textContent = typeof t === 'function' ? t('ssh.signatureVerified') : '签名验证通过'; importBtn.disabled = false; } else { resultBox.className = 'result-box error'; @@ -21406,12 +21477,12 @@ async function confirmSourceImport() { const importBtn = document.getElementById('import-source-btn'); if (!window._importSourceTscfg) { - showToast('请先选择文件', 'error'); + showToast((typeof t === 'function' ? t('toast.selectFileFirst') : '请先选择文件'), 'error'); return; } resultBox.classList.remove('hidden', 'success', 'error'); - resultBox.textContent = '正在保存配置...'; + resultBox.textContent = typeof t === 'function' ? t('ssh.savingConfig') : '正在保存配置...'; importBtn.disabled = true; try { @@ -21427,18 +21498,18 @@ async function confirmSourceImport() { const data = result.data; if (data?.exists && !data?.imported) { resultBox.className = 'result-box warning'; - resultBox.textContent = `配置 ${data.id} 已存在,请勾选「覆盖」选项`; + resultBox.textContent = typeof t === 'function' ? t('securityPage.configExistsCheckOverwrite', { id: data.id }) : `配置 ${data.id} 已存在,请勾选「覆盖」选项`; importBtn.disabled = false; } else { resultBox.className = 'result-box success'; resultBox.innerHTML = `${typeof t === 'function' ? t('securityPage.savedConfig') : 'Saved config'}: ${escapeHtml(data?.id)}
${typeof t === 'function' ? t('securityPage.restartToApply') : 'Restart to apply'}`; - showToast(`已导入配置,重启后生效`, 'success'); + showToast(typeof t === 'function' ? t('toast.configImported') : '已导入配置,重启后生效', 'success'); setTimeout(() => hideImportSourceModal(), 2000); } } else { resultBox.className = 'result-box error'; - resultBox.textContent = (result.message || '导入失败'); - importBtn.disabled = false; +resultBox.textContent = (result.message || (typeof t === 'function' ? t('toast.importFailed') : '导入失败')); + importBtn.disabled = false; } } catch (e) { resultBox.className = 'result-box error'; @@ -21462,20 +21533,20 @@ function showExportRuleModal(ruleId) { modal.innerHTML = ` @@ -21495,7 +21566,7 @@ async function doExportRule(ruleId) { const exportBtn = document.getElementById('export-rule-btn'); resultBox.classList.remove('hidden', 'success', 'error'); - resultBox.textContent = '正在生成配置包...'; + resultBox.textContent = typeof t === 'function' ? t('securityPage.generatingPack') : '正在生成配置包...'; exportBtn.disabled = true; try { @@ -21520,8 +21591,8 @@ async function doExportRule(ruleId) { URL.revokeObjectURL(url); resultBox.className = 'result-box success'; - resultBox.textContent = '导出成功'; - showToast(`已导出规则配置: ${data.filename}`, 'success'); + resultBox.textContent = typeof t === 'function' ? t('toast.exportSuccess') : '导出成功'; + showToast(typeof t === 'function' ? t('toast.ruleConfigExported', { filename: data.filename }) : `已导出规则配置: ${data.filename}`, 'success'); setTimeout(() => hideExportRuleModal(), 1000); } catch (e) { resultBox.className = 'result-box error'; @@ -21625,12 +21696,12 @@ async function previewRuleImport() { `; if (data.exists) { - html += `
该配置已存在,导入将覆盖现有文件
`; + html += `
${typeof t === 'function' ? t('securityPage.configExistsWarning') : '该配置已存在,导入将覆盖现有文件'}
`; } previewDiv.innerHTML = html; step2.style.display = 'block'; resultBox.className = 'result-box success'; - resultBox.textContent = '签名验证通过'; + resultBox.textContent = typeof t === 'function' ? t('ssh.signatureVerified') : '签名验证通过'; importBtn.disabled = false; } else { resultBox.className = 'result-box error'; @@ -21648,12 +21719,12 @@ async function confirmRuleImport() { const importBtn = document.getElementById('import-rule-btn'); if (!window._importRuleTscfg) { - showToast('请先选择文件', 'error'); + showToast((typeof t === 'function' ? t('toast.selectFileFirst') : '请先选择文件'), 'error'); return; } resultBox.classList.remove('hidden', 'success', 'error'); - resultBox.textContent = '正在保存配置...'; + resultBox.textContent = typeof t === 'function' ? t('ssh.savingConfig') : '正在保存配置...'; importBtn.disabled = true; try { @@ -21669,17 +21740,17 @@ async function confirmRuleImport() { const data = result.data; if (data?.exists && !data?.imported) { resultBox.className = 'result-box warning'; - resultBox.textContent = `配置 ${data.id} 已存在,请勾选「覆盖」选项`; + resultBox.textContent = typeof t === 'function' ? t('securityPage.configExistsCheckOverwrite', { id: data.id }) : `配置 ${data.id} 已存在,请勾选「覆盖」选项`; importBtn.disabled = false; } else { resultBox.className = 'result-box success'; resultBox.innerHTML = `${typeof t === 'function' ? t('securityPage.savedConfig') : 'Saved config'}: ${escapeHtml(data?.id)}
${typeof t === 'function' ? t('securityPage.restartToApply') : 'Restart to apply'}`; - showToast(`已导入配置,重启后生效`, 'success'); + showToast(typeof t === 'function' ? t('toast.configImported') : '已导入配置,重启后生效', 'success'); setTimeout(() => hideImportRuleModal(), 2000); } } else { resultBox.className = 'result-box error'; - resultBox.textContent = (result.message || '导入失败'); + resultBox.textContent = (result.message || (typeof t === 'function' ? t('toast.importFailed') : '导入失败')); importBtn.disabled = false; } } catch (e) { @@ -21867,12 +21938,12 @@ async function previewActionImport() { `; if (data.exists) { - html += `
该配置已存在,导入将覆盖现有文件
`; + html += `
${typeof t === 'function' ? t('securityPage.configExistsWarning') : '该配置已存在,导入将覆盖现有文件'}
`; } previewDiv.innerHTML = html; step2.style.display = 'block'; resultBox.className = 'result-box success'; - resultBox.textContent = '签名验证通过'; + resultBox.textContent = typeof t === 'function' ? t('ssh.signatureVerified') : '签名验证通过'; importBtn.disabled = false; } else { resultBox.className = 'result-box error'; @@ -21890,12 +21961,12 @@ async function confirmActionImport() { const importBtn = document.getElementById('import-action-btn'); if (!window._importActionTscfg) { - showToast('请先选择文件', 'error'); + showToast((typeof t === 'function' ? t('toast.selectFileFirst') : '请先选择文件'), 'error'); return; } resultBox.classList.remove('hidden', 'success', 'error'); - resultBox.textContent = '正在保存配置...'; + resultBox.textContent = typeof t === 'function' ? t('ssh.savingConfig') : '正在保存配置...'; importBtn.disabled = true; try { @@ -21911,17 +21982,17 @@ async function confirmActionImport() { const data = result.data; if (data?.exists && !data?.imported) { resultBox.className = 'result-box warning'; - resultBox.textContent = `配置 ${data.id} 已存在,请勾选「覆盖」选项`; + resultBox.textContent = typeof t === 'function' ? t('securityPage.configExistsCheckOverwrite', { id: data.id }) : `配置 ${data.id} 已存在,请勾选「覆盖」选项`; importBtn.disabled = false; } else { resultBox.className = 'result-box success'; resultBox.innerHTML = `${typeof t === 'function' ? t('securityPage.savedConfig') : 'Saved config'}: ${escapeHtml(data?.id)}
${typeof t === 'function' ? t('securityPage.restartToApply') : 'Restart to apply'}`; - showToast(`已导入配置,重启后生效`, 'success'); + showToast(typeof t === 'function' ? t('toast.configImported') : '已导入配置,重启后生效', 'success'); setTimeout(() => hideImportActionModal(), 2000); } } else { resultBox.className = 'result-box error'; - resultBox.textContent = (result.message || '导入失败'); + resultBox.textContent = (result.message || (typeof t === 'function' ? t('toast.importFailed') : '导入失败')); importBtn.disabled = false; } } catch (e) { diff --git a/components/ts_webui/web/js/lang/en-US.js b/components/ts_webui/web/js/lang/en-US.js index 28f1a76..7c15f23 100644 --- a/components/ts_webui/web/js/lang/en-US.js +++ b/components/ts_webui/web/js/lang/en-US.js @@ -141,7 +141,11 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { low: 'Low', host: 'Host', device: 'Device', - logs: 'Logs' + logs: 'Logs', + loadFailed: 'Load Failed', + loadFailedMsg: 'Load failed: {msg}', + refreshOnce: 'Refresh once', + uploadFailedMsg: 'Failed: {msg}' }, // Status labels @@ -340,7 +344,16 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { maxCurvePoints: 'Maximum 10 curve points supported', statusUnavailable: 'Fan status unavailable', importConfig: 'Import Config', - exportConfig: 'Export Config' + exportConfig: 'Export Config', + enterValidTemp: 'Please enter a valid temperature (0-100°C)', + testTempCleared: 'Test temperature cleared, resumed normal mode', + testTempSet: 'Test temperature set to {temp}°C', + speedSet: 'Fan {id} speed set to {speed}%', + modeSwitch: 'Fan {id} mode switched to {mode}', + setFanFailed: 'Failed to set fan: {msg}', + setFanModeFailed: 'Failed to set fan mode: {msg}', + setFailed: 'Setting failed: {msg}', + clearFailed: 'Clear failed: {msg}' }, // LED Control @@ -379,7 +392,8 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { browseImages: 'Browse Images', touch: 'Touch Buttons', board: 'Board LED', - matrix: 'LED Matrix' + matrix: 'LED Matrix', + selectedFilter: 'Selected: {filter}' }, // Network @@ -671,7 +685,9 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { partitionIdle: 'Idle', disableRollbackDesc: 'Disable auto rollback protection', rollbackToThis: 'Rollback to this version', - loadAfterReboot: 'Load this partition after reboot' + loadAfterReboot: 'Load this partition after reboot', + firmwareDownloadComplete: 'Firmware download complete ({size})', + webuiDownloadComplete: 'WebUI download complete ({size})' }, // Automation @@ -698,6 +714,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { ruleName: 'Rule Name', conditions: 'Conditions', conditionLogic: 'Logic', + ruleOptions: 'Rule options', logicAnd: 'All (AND)', logicOr: 'Any (OR)', cooldown: 'Cooldown', @@ -936,6 +953,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { welcome: 'Welcome to TianShanOS', username: 'Username', password: 'Password', + passwordPlaceholder: 'Please enter password', rememberMe: 'Remember Me', loginButton: 'Login', loggingIn: 'Logging in...', @@ -1032,6 +1050,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { imported: 'Import complete', loggedOut: 'Logged out', logsLoaded: 'Logs loaded', + logsLoadedCount: 'Loaded {count} log entries', wifiDisconnected: 'WiFi disconnected', natEnabled: 'NAT enabled', natDisabled: 'NAT disabled', @@ -1092,8 +1111,54 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { serviceSuccess: 'Service {name} {action} successful', // Voltage protection voltageProtectionEnabled: 'Voltage protection enabled', + lowVoltageWarning: 'Low voltage warning: {voltage}V ({countdown}s)', + voltageProtectionTriggered: 'Voltage protection triggered', + voltageRecovering: 'Voltage recovering: {voltage}V', voltageProtectionDisabled: 'Voltage protection disabled', switchFailed: 'Switch failed', + switchFailedMsg: 'Switch failed: {msg}', + setFailedMsg: 'Setting failed: {msg}', + enterHostname: 'Please enter hostname', + hostnameSet: 'Hostname set', + connectFailedMsg: 'Connection failed: {msg}', + disconnectFailedMsg: 'Disconnect failed: {msg}', + configFailedMsg: 'Config failed: {msg}', + operationFailedMsg: 'Operation failed: {msg}', + applyFailedMsg: 'Apply failed: {msg}', + saveFailedMsg: 'Save failed: {msg}', + deleteFailedMsg: 'Delete failed: {msg}', + removeFailedMsg: 'Remove failed: {msg}', + exportFailedMsg: 'Export failed: {msg}', + updateFailedMsg: 'Update failed: {msg}', + clearFailedMsg: 'Clear failed: {msg}', + copyFailedMsg: 'Copy failed: {msg}', + upgradeFailedMsg: 'Upgrade failed: {msg}', + webuiUpgradeStartFailed: 'WebUI upgrade start failed', + upgradeStartFailedMsg: 'Upgrade start failed: {msg}', + rollbackFailedMsg: 'Rollback failed: {msg}', + abortFailedMsg: 'Abort failed: {msg}', + otaServerNotSet: 'OTA server address not set', + firmwareWriteComplete: 'Firmware write complete!', + webuiWriteComplete: 'WebUI write complete!', + webuiUpgradeSkipped: 'WebUI upgrade skipped: {msg}', + nohupStripped: 'Auto-removed redundant nohup wrapper (backend adds it)', + saveCommandFailedMsg: 'Failed to save command: {msg}', + deleteCommandFailedMsg: 'Failed to delete command: {msg}', + fillServerInfo: 'Please fill in complete server information', + publicKeyNotFound: 'Public key not found', + oldHostKeyRemoved: 'Old host key removed, reconnect to trust new key', + allHostsCleared: 'All known hosts cleared', + generateFailedMsg: 'Generate failed: {msg}', + configPackDownloaded: 'Config pack downloaded: {filename}', + selectAtLeastOneAction: 'Please select at least one action template', + cmdNotExist: 'Command does not exist, please reselect', + hostFingerprintDeleted: 'Host fingerprint deleted', + keyDeleted: 'Key deleted', + cannotGetPublicKey: 'Cannot get public key', + cannotGetPrivateKey: 'Cannot get private key', + keyRevoked: 'Key revoked successfully', + hostRemoved: 'Host removed', + configPackCopied: 'Config pack copied to clipboard', // USB switch usbSwitching: 'Switching USB to {target}...', usbSwitched: 'USB switched to {target}', @@ -1104,14 +1169,29 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { widgetSaved: 'Widget saved', widgetDeleted: 'Deleted {name}', widgetBound: 'Bound {widget} → {var}', - // Quick actions + // Quick actions / data + agxMovedToBinding: 'AGX config moved to variable binding', + waitBeforeTrigger: 'Please wait a few seconds before triggering another', + operationExecuted: 'Operation executed', + execFailed: 'Execution failed', + execFailedMsg: 'Execution failed: {msg}', + errorMsg: 'Error: {msg}', processRunning: 'Process is running, please stop it first', + confirmTerminateCmd: 'Are you sure you want to stop "{cmdName}"?', + terminatingProcess: 'Stopping process...', cardNotFound: 'Action card not found', executionFailed: 'Execution failed', processTerminated: 'Process terminated', processNotExist: 'Process does not exist', pidFileNotExist: 'PID file does not exist', operationComplete: 'Operation complete', + // Terminate process results + terminateTERMINATED: 'Service stopped', + terminateFORCE_KILLED: 'Service force terminated', + terminateALREADY_STOPPED: 'Process no longer running, PID file cleaned', + terminateNO_PID_FILE: 'PID file does not exist', + terminateSTILL_RUNNING: 'Cannot terminate process, please handle manually', + terminateDefault: 'Operation complete', // Time sync timeSynced: 'Time synced: {datetime}', timeSyncSyncing: 'Syncing time from browser...', @@ -1132,12 +1212,30 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { ledQrFailed: 'Failed to generate QR code', ledTextFailed: 'Failed to display text', ledTextStopFailed: 'Failed to stop text', + allLedsOff: 'All LEDs turned off', selectAnimation: 'Please select an animation first', + enterImagePath: 'Please enter image path', + imageDisplayed: 'Image displayed', + enterTextToEncode: 'Please enter text to encode', + qrGenerated: 'QR code generated', + enterTextToDisplay: 'Please enter text to display', + textDisplayed: 'Text displayed', + textScrollStopped: 'Text scroll stopped', + filterStopped: 'Filter stopped', + selectFilter: 'Please select a filter first', ledBrightnessSet: '{device} brightness: {value}', ledBrightnessFailed: 'Failed to set {device} brightness', ledTurnedOff: '{device} turned off', ledTurnedOn: '{device} turned on', ledOnFailed: 'Failed to turn on', + ledOffFailed: 'Failed to turn off', + filterApplied: 'Filter {filter} applied', + filterApplyFailed: 'Failed to apply filter', + filterStopFailed: 'Failed to stop filter', + ledConfigSaved: '{device} config saved', + ledConfigSavedWithAnim: '{device} config saved: {anim}', + ledConfigSaveFailed: 'Failed to save config', + enterQrContent: 'Please enter QR code content', // PKI/Certificates configPackImported: 'Config pack imported successfully', keypairGenerated: 'Key pair generated successfully', @@ -1186,6 +1284,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { ruleTriggered: 'Rule {id} triggered', toggleRuleFailed: 'Failed to toggle rule', triggerRuleFailed: 'Failed to trigger rule', + toggleSourceFailed: 'Failed to toggle source status', sourceToggled: 'Source {id} {state}', actionExecuting: 'Executing action: {id}...', actionSuccess: 'Execution successful', @@ -1201,6 +1300,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { shutdownSettingsSaved: 'Shutdown settings saved', defaultsRestored: 'Defaults restored', restoreFailed: 'Restore failed', + restoreFailedMsg: 'Restore failed: {msg}', deleteRuleResult: 'Delete rule {id}', deleteRuleFailed: 'Failed to delete rule', getRuleDetailFailed: 'Failed to get rule details', @@ -1209,7 +1309,12 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { createActionFirst: 'Please create action templates first', ruleCreated: 'Rule {id} {action} successfully', ruleCreateFailed: 'Failed to {action} rule', + actionResult: '{action}: {msg}', + actionFailedMsg: '{action} failed: {msg}', + ruleUpdate: 'Update', + ruleCreate: 'Create', configExported: 'Exported {type} config: {filename}', + ruleConfigExported: 'Exported rule config: {filename}', selectFile: 'Please select a file first', configImported: 'Config imported, takes effect after restart', // Form validation @@ -1241,14 +1346,32 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { // Network wifiModeChanged: 'WiFi mode changed to {mode}', // File operations + createDirFailed: 'Failed to create directory: {msg}', + enterQrContent: 'Please enter QR code content', + enterDisplayText: 'Please enter display text', + textStopped: 'Text stopped', + selectFileToDelete: 'Please select file(s) to delete first', + selectFileToDownload: 'Please select file(s) to download first', noDownloadableFiles: 'No downloadable files in selection (folders not supported)', + batchDownloadComplete: 'Batch download complete', + deletingItems: 'Deleting {count} items...', + deleteSuccessCount: 'Successfully deleted {count} items', + deletePartial: 'Delete complete: {success} success, {fail} failed', + selectFileToUpload: 'Please select file(s) to upload', + uploadComplete: 'Upload complete', downloadingFiles: 'Downloading {count} files...', sdCardUnmounted: 'SD card unmounted', unmountFailed: 'Unmount failed', enterFolderName: 'Please enter folder name', folderCreated: 'Folder created successfully', + createFailedMsg: 'Create failed: {msg}', enterNewName: 'Please enter new name', renameSuccess: 'Rename successful', + renameFailedMsg: 'Rename failed: {msg}', + downloadStart: 'Download started', + downloadFailedMsg: 'Download failed: {msg}', + deleteSuccess: 'Deleted successfully', + deleteFailedMsg: 'Delete failed: {msg}', // SSH page selectHostFirst: 'Please select a host first', fillCommandNameAndCmd: 'Please fill in command name and command', @@ -1268,17 +1391,21 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { processAlreadyStopped: 'Process already stopped', serviceNotRunning: 'Service not running', stopServiceFailed: 'Failed to stop service', + stopServiceFailedMsg: 'Failed to stop service: {msg}', commandRunning: 'A command is running, please cancel or wait for completion', startExecFailed: 'Failed to start execution', + startExecFailedMsg: 'Failed to start execution: {msg}', noRunningCommand: 'No running command', cancelSent: 'Cancel request sent', cancelFailed: 'Cancel failed', + cancelFailedMsg: 'Cancel failed: {msg}', patternMatchSuccess: 'Pattern matched successfully', commandSuccess: 'Command executed successfully', commandMatchFailed: 'Command completed, pattern match failed', commandTimeout: 'Command execution timeout', commandCompletedCode: 'Command completed, exit code: {code}', execError: 'Execution error', + execErrorMsg: 'Execution error: {msg}', commandCancelled: 'Command cancelled', hostFingerprintDeleted: 'Host fingerprint deleted', hostInfoEmpty: 'Host information is empty', @@ -1313,6 +1440,8 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { selectedJsonFiles: 'Selected all JSON files in current directory', configPackCopied: 'Config pack copied to clipboard', configPackDownloaded: 'Config pack downloaded: {filename}', + configPackVerifySuccess: 'Config pack verified\nSigner: {signer} {isOfficial}', + configPackVerifyFailed: 'Config pack verification failed: {msg}', // OTA otaUpgradeFailed: 'Upgrade failed', webuiUpgradeFailed: 'WebUI upgrade failed to start', @@ -1536,8 +1665,10 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { statusFailed: 'Failed', statusIdle: 'Idle', statusStopped: 'Stopped', + statusUnknown: 'Unknown', // Error messages hostNotFound: 'Host not found', + hostNotExistShort: 'Host does not exist', cmdNotFound: 'Command not found', // Match result labels matchResultTitle: 'Match Results', @@ -1568,9 +1699,12 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { selectDevice: '-- Select Device --', noKeysHint: 'No keys yet, click button above to generate', noDeployedHint: 'No deployed hosts yet, please click "Deploy" in key management above', + noDeployedHostsMsg: 'No deployed hosts', + deployKeyAtSecurityHint: 'Please go to Security page to deploy SSH public key first', noKnownHosts: 'No known host fingerprints', pleaseConfigSshHost: '-- Please configure SSH host first --', noVariableData: 'No variable data for this command, please execute it first', + getVarFailed: 'Failed to get variables', // Variable table headers varTableName: 'Variable Name', varTableType: 'Type', @@ -1622,9 +1756,17 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { stopServiceFailed: 'Failed to stop service', executionFailed: 'Execution failed', getLogFailed: 'Failed to get log', + getLogFailedMsg: 'Failed to fetch log: {msg}', + startRealTimeTail: 'Starting real-time tail: {logFile}\n(Click "Stop Tracking" to exit)', + viewServiceLogFile: 'View service log: {name}\nFile: {file}', + logErrorMsg: '[Error] {msg}', + stopServiceName: 'Stop service: {name}', + startExecFailedDetail: 'Failed to start execution\n\n{msg}', patternMatchSuccess: 'Pattern Match Success!', expectPatternMatch: 'Expect pattern matched: Yes', failPatternMatch: 'Fail pattern matched: Yes', + failMatchedTrue: 'true (error detected)', + failMatchedFalse: 'false', extractedContent: 'Extracted Content', cancelledExecution: 'Execution cancelled', error: 'Error', @@ -2167,6 +2309,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { // OTA states stateIdle: 'Idle', stateChecking: 'Checking...', + downloading: 'Downloading...', downloadingWebUI: 'Downloading WebUI...', downloadingFirmware: 'Downloading firmware...', stateVerifying: 'Verifying...', @@ -2204,7 +2347,13 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { refreshPage: 'Refresh Page', currentVersionUnknown: 'Unknown', step1: '[1/2] Upgrading Firmware', - step2: '[2/2] Upgrading WebUI' + step2: '[2/2] Upgrading WebUI', + step1Connecting: '[1/2] Connecting to server...', + preparing: 'Preparing...', + writing: 'Writing...', + readingFile: 'Reading file...', + stateError: 'Error', + connecting: 'Connecting...' }, // Automation Page @@ -2255,7 +2404,12 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { requestBodyLabel: 'Request Body', requestBodyHint: 'JSON format, supports variables', getMemoryDetailFailed: 'Failed to get memory details', - // Logs + // Logs (quick action log tail) + logFetchFailed: '[Fetch failed]', + logError: '[Error]', + deviceBusyRetry: 'Device may be busy, please retry later.', + logEmptyBracket: '[Empty]', + logFileEmptyBracket: '[Log file does not exist or is empty]', logFileNotExist: 'Log file does not exist or is empty', logEmpty: 'No logs', noRules: 'No rules yet, click "Add" to create', @@ -2319,6 +2473,10 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { logLevel: 'Level', logMessage: 'Message', logMsgHint: 'Supports variables: ${var_name}', + varConfig: 'Variable Config', + varNameLabel: 'Variable Name', + method: 'Method', + requestBody: 'Request Body', indexPlaceholder: 'Index', pulseMsPlaceholder: 'Pulse ms', varNamePlaceholder: 'Var name', @@ -2372,6 +2530,12 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { stopTracking: 'Stop Tracking', startTracking: 'Start Tracking', interval: 'Interval', + interval1Sec: '1s', + interval2Sec: '2s', + interval3Sec: '3s', + interval5Sec: '5s', + interval10Sec: '10s', + interval30Sec: '30s', realTimeUpdating: '● Real-time updating', trackingStopped: 'Tracking stopped', // Action editor labels @@ -2434,6 +2598,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { ledOffHint: 'Turn off LED device, no extra parameters needed', filterStopHint: 'Stop currently running filter effect, no extra parameters needed', textStopHint: 'Stop currently running text overlay, no extra parameters needed', + textContentLabel: 'Text Content', textPlaceholder: 'Text to display, supports ${variable}', scroll: 'Scroll', scrollNone: 'No Scroll', @@ -2665,6 +2830,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { secondaryExpressionPlaceholder: 'e.g.: ${max_value}', displayLines: 'Display Lines', refreshIntervalMs: 'Refresh Interval (ms)', + configLogVariableFirst: 'Please configure log variable first', logVariable: 'Log Variable', logVariablePlaceholder: 'Select variable containing log text', selectVariable: 'Select Variable', @@ -2740,6 +2906,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { // Modal - Effect effectTitle: 'Programmatic Animation', effectNotSelected: 'None', + current: 'Current', speed: 'Speed', color: 'Color', start: 'Start', @@ -2887,17 +3054,30 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { remainingDays: '{days} days remaining', generatingKeyPair: 'Generating key pair...', generatingCsr: 'Generating CSR...', + ecdsaKeyPairSuccess: 'ECDSA P-256 key pair generated successfully!', + installingCert: 'Installing certificate...', + certInstalledSuccess: 'Certificate installed successfully!', installFailed: 'Install failed', + installFailedMsg: 'Install failed: {msg}', + deployFailedMsg: 'Deploy failed: {msg}', + revokeFailedMsg: 'Revoke failed: {msg}', + invalidJsonConfigMsg: 'Config file is not valid JSON: {msg}', // Config Pack enterConfigName: 'Please enter config name', selectConfigFile: 'Please select config file', pasteTargetCert: 'Please paste target device certificate', invalidJsonConfig: 'Config file is not valid JSON', generatingPack: 'Generating config pack', + generatingPackWithCount: 'Generating config pack ({count} files)...', savedToDevice: 'Saved to device', + installingCaCert: 'Installing CA certificate chain...', + caCertInstalledSuccess: 'CA certificate chain installed successfully!', + loadFailedMsg: 'Load failed: {msg}', generationFailed: 'Generation failed', + generationFailedMsg: 'Generation failed: {msg}', verifying: 'Verifying...', verifyFailed: 'Verification failed', + signatureInvalid: 'Invalid signature', importing: 'Importing...', importFailed: 'Import failed', uploadOrPasteContent: 'Please upload file or paste config pack content' @@ -2926,6 +3106,8 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { turnOn: 'On', colorSettings: 'Color Settings', turnOffLight: 'Turn Off', + turnOnLight: 'Turn On', + noFilterSelected: 'No filter selected', // OTA firmwareUpgradeComplete: 'Firmware upgrade complete, preparing WebUI upgrade...', allUpgradeComplete: 'All upgrades complete', @@ -2956,6 +3138,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { newCommand: '+ New Command', editCommand: 'Edit Command', commandVariables: 'Command Variables', + commandVariablesWithName: 'Command variables: {varName}.*', sourceVariables: '{source} Variables', editActionTemplate: 'Edit Action Template', // Actions @@ -3044,6 +3227,10 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { confirmRemoveKnownHost: 'Are you sure you want to remove the record for "{host}:{port}"?', confirmClearKnownHosts: 'Are you sure you want to clear all known host records? This action cannot be undone!', confirmDeletePKI: 'Are you sure you want to delete all PKI credentials?\n\nThis will delete:\n• Private key\n• Device certificate\n• CA certificate chain\n\nThis action cannot be undone!', + confirmMarkFirmwareValid: 'Confirm marking current firmware as valid?\nThis will disable automatic rollback protection.', + confirmRollback: 'Confirm rollback to previous firmware version?\n\nThe system will immediately reboot and load the firmware from the previous partition.\nPlease ensure the previous firmware is available!', + confirmAbortUpgrade: 'Confirm abort current upgrade?', + confirmRestoreDefaults: 'Confirm restore default settings?', // Alert dialogs and other prompts alertHostFingerprint: 'Host: {host}:{port}\nType: {type}\nFingerprint (SHA256):\n{fingerprint}', alertEnterWsAddress: 'Please enter WebSocket address', @@ -3087,7 +3274,29 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', { connecting: 'Connecting to device...', connected: 'Connected to device', helpHint: 'Enter {help} to view available commands', + connectionDisconnected: 'Connection disconnected', + reconnectIn: 'Reconnecting in 5 seconds...', + reconnecting: 'Reconnecting...', + connectionError: 'Connection error', + notConnected: 'Not connected to device', + sshShellRequiresParams: 'Error: SSH shell requires --host and --user parameters', + sshSessionInProgress: 'SSH session already in progress', + sshConnectingTo: 'Connecting to {user}@{host}:{port}...', + exitSshHint: '(Press Ctrl+\\ to exit SSH shell)', + exitSshDisplay: '^\\ (Exit SSH shell)', + lowVoltageWarning: '⚠️ Low voltage warning! Voltage: {voltage}V - Shutdown countdown started', + countdownTick: '⏱️ Shutdown countdown: {countdown}s | Voltage: {voltage}V', + shutdownStart: '🔴 Shutting down... Voltage: {voltage}V', + protected: '🛡️ Protection active | Waiting for voltage recovery...', + recoveryStart: '🔄 Voltage recovering: {voltage}V | Waiting for stability...', + recoveryComplete: '✅ Voltage recovery complete! {voltage}V | System restarting soon', + debugTick: '📊 [Debug] {state} | Voltage: {voltage}V | Countdown: {countdown}s', + voltageNormal: '✓ Voltage normal: {voltage}V', + stateChanged: 'State changed: {state} | Voltage: {voltage}V', + powerEvent: '[Power] {event}: state={state}, voltage={voltage}V', pageTitle: 'Web Terminal', + errorLabel: 'Error', + unknownError: 'Unknown error', terminalHint: 'Tip: Type help for commands | Ctrl+C interrupt | Ctrl+L clear | ↑↓ history', systemLogTitle: 'System Logs', levelLabel: 'Level', diff --git a/components/ts_webui/web/js/lang/zh-CN.js b/components/ts_webui/web/js/lang/zh-CN.js index f59398d..435728f 100644 --- a/components/ts_webui/web/js/lang/zh-CN.js +++ b/components/ts_webui/web/js/lang/zh-CN.js @@ -143,6 +143,9 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { device: '设备', logs: '日志', loadFailed: '加载失败', + loadFailedMsg: '加载失败: {msg}', + refreshOnce: '刷新一次', + uploadFailedMsg: '失败: {msg}', copyToClipboard: '复制到剪贴板', selectFile: '选择文件', install: '安装' @@ -344,7 +347,16 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { maxCurvePoints: '最多支持 10 个曲线点', statusUnavailable: '风扇状态不可用', importConfig: '导入配置', - exportConfig: '导出配置' + exportConfig: '导出配置', + enterValidTemp: '请输入有效温度 (0-100°C)', + testTempCleared: '测试温度已清除,恢复正常模式', + testTempSet: '测试温度已设置为 {temp}°C', + speedSet: '风扇 {id} 速度已设置为 {speed}%', + modeSwitch: '风扇 {id} 模式已切换为 {mode}', + setFanFailed: '设置风扇失败: {msg}', + setFanModeFailed: '设置风扇模式失败: {msg}', + setFailed: '设置失败: {msg}', + clearFailed: '清除失败: {msg}' }, // LED 控制 @@ -383,7 +395,8 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { browseImages: '浏览图像', touch: '触摸按钮', board: '板载 LED', - matrix: 'LED 矩阵' + matrix: 'LED 矩阵', + selectedFilter: '已选择: {filter}' }, // 网络 @@ -679,7 +692,9 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { partitionIdle: '空闲', disableRollbackDesc: '取消自动回滚保护', rollbackToThis: '回滚到此版本', - loadAfterReboot: '重启后加载此分区' + loadAfterReboot: '重启后加载此分区', + firmwareDownloadComplete: '固件下载完成 ({size})', + webuiDownloadComplete: 'WebUI 下载完成 ({size})' }, // 自动化 @@ -706,6 +721,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { ruleName: '规则名称', conditions: '触发条件', conditionLogic: '条件逻辑', + ruleOptions: '规则选项', logicAnd: '全部满足 (AND)', logicOr: '任一满足 (OR)', cooldown: '冷却时间', @@ -937,6 +953,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { welcome: '欢迎使用 TianShanOS', username: '用户名', password: '密码', + passwordPlaceholder: '请输入密码', rememberMe: '记住我', loginButton: '登录', loggingIn: '登录中...', @@ -1033,6 +1050,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { imported: '导入完成', loggedOut: '已退出登录', logsLoaded: '加载完成', + logsLoadedCount: '加载了 {count} 条历史日志', wifiDisconnected: 'WiFi 已断开', natEnabled: 'NAT 已启用', natDisabled: 'NAT 已禁用', @@ -1057,8 +1075,54 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { serviceSuccess: '服务 {name} {action} 成功', // 电压保护 voltageProtectionEnabled: '电压保护已启用', + lowVoltageWarning: '低电压警告: {voltage}V ({countdown}s)', + voltageProtectionTriggered: '电压保护已触发', + voltageRecovering: '电压恢复中: {voltage}V', voltageProtectionDisabled: '电压保护已禁用', switchFailed: '切换失败', + switchFailedMsg: '切换失败: {msg}', + setFailedMsg: '设置失败: {msg}', + enterHostname: '请输入主机名', + hostnameSet: '主机名已设置', + connectFailedMsg: '连接失败: {msg}', + disconnectFailedMsg: '断开失败: {msg}', + configFailedMsg: '配置失败: {msg}', + operationFailedMsg: '操作失败: {msg}', + applyFailedMsg: '应用失败: {msg}', + saveFailedMsg: '保存失败: {msg}', + deleteFailedMsg: '删除失败: {msg}', + removeFailedMsg: '移除失败: {msg}', + exportFailedMsg: '导出失败: {msg}', + updateFailedMsg: '更新失败: {msg}', + clearFailedMsg: '清除失败: {msg}', + copyFailedMsg: '复制失败: {msg}', + upgradeFailedMsg: '升级失败: {msg}', + webuiUpgradeStartFailed: 'WebUI 升级启动失败', + upgradeStartFailedMsg: '启动升级失败: {msg}', + rollbackFailedMsg: '回滚失败: {msg}', + abortFailedMsg: '中止失败: {msg}', + otaServerNotSet: 'OTA 服务器地址未设置', + firmwareWriteComplete: '固件写入完成!', + webuiWriteComplete: 'WebUI 写入完成!', + webuiUpgradeSkipped: 'WebUI 升级跳过: {msg}', + nohupStripped: '已自动去除命令中多余的 nohup 包装(后端会自动添加)', + saveCommandFailedMsg: '保存指令失败: {msg}', + deleteCommandFailedMsg: '删除指令失败: {msg}', + fillServerInfo: '请填写完整的服务器信息', + publicKeyNotFound: '公钥未找到', + oldHostKeyRemoved: '旧主机密钥已移除,请重新连接以信任新密钥', + allHostsCleared: '已清除所有已知主机', + generateFailedMsg: '生成失败: {msg}', + configPackDownloaded: '配置包已下载: {filename}', + selectAtLeastOneAction: '请至少选择一个动作模板', + cmdNotExist: '指令不存在,请重新选择', + hostFingerprintDeleted: '已删除主机指纹', + keyDeleted: '密钥已删除', + cannotGetPublicKey: '无法获取公钥', + cannotGetPrivateKey: '无法获取私钥', + keyRevoked: '密钥撤销成功', + hostRemoved: '主机已移除', + configPackCopied: '配置包已复制到剪贴板', // USB 切换 usbSwitching: '切换 USB 到 {target}...', usbSwitched: 'USB 已切换到 {target}', @@ -1069,14 +1133,29 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { widgetSaved: '组件已保存', widgetDeleted: '已删除 {name}', widgetBound: '已绑定 {widget} → {var}', - // 快捷操作 + // 快捷操作 / 数据 + agxMovedToBinding: 'AGX 配置已移至变量绑定', + waitBeforeTrigger: '请等待几秒后再触发其他模型', + operationExecuted: '操作已执行', + execFailed: '执行失败', + execFailedMsg: '执行失败: {msg}', + errorMsg: '错误: {msg}', processRunning: '进程正在运行中,请先停止', + confirmTerminateCmd: '确定要终止 "{cmdName}" 吗?', + terminatingProcess: '正在终止进程...', cardNotFound: '无法找到操作卡片', executionFailed: '执行失败', processTerminated: '已终止进程', processNotExist: '进程已不存在', pidFileNotExist: 'PID 文件不存在', operationComplete: '操作完成', + // 终止进程结果 + terminateTERMINATED: '服务已停止', + terminateFORCE_KILLED: '服务已强制终止', + terminateALREADY_STOPPED: '进程已不在运行,已清理 PID 文件', + terminateNO_PID_FILE: 'PID 文件不存在', + terminateSTILL_RUNNING: '无法终止进程,请手动处理', + terminateDefault: '操作完成', // 时间同步 timeSynced: '时间已同步: {datetime}', timeSyncSyncing: '正在从浏览器同步时间...', @@ -1097,12 +1176,30 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { ledQrFailed: '生成 QR 码失败', ledTextFailed: '显示文本失败', ledTextStopFailed: '停止文本失败', + allLedsOff: '全部 LED 已关闭', selectAnimation: '请先选择一个动画', + enterImagePath: '请输入图像路径', + imageDisplayed: '图像已显示', + enterTextToEncode: '请输入要编码的文本', + qrGenerated: 'QR 码已生成', + enterTextToDisplay: '请输入要显示的文本', + textDisplayed: '文本已显示', + textScrollStopped: '文本滚动已停止', + filterStopped: '滤镜已停止', + selectFilter: '请先选择滤镜', ledBrightnessSet: '{device} 亮度: {value}', ledBrightnessFailed: '设置 {device} 亮度失败', ledTurnedOff: '{device} 已关闭', ledTurnedOn: '{device} 已开启', ledOnFailed: '开启失败', + ledOffFailed: '关闭失败', + filterApplied: '滤镜 {filter} 已应用', + filterApplyFailed: '应用滤镜失败', + filterStopFailed: '停止滤镜失败', + ledConfigSaved: '{device} 配置已保存', + ledConfigSavedWithAnim: '{device} 配置已保存: {anim}', + ledConfigSaveFailed: '保存配置失败', + enterQrContent: '请输入QR码内容', // PKI/证书 configPackImported: '配置包导入成功', importFailed: '导入失败', @@ -1155,6 +1252,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { ruleTriggered: '触发规则 {id}', toggleRuleFailed: '切换规则状态失败', triggerRuleFailed: '触发规则失败', + toggleSourceFailed: '切换数据源状态失败', sourceToggled: '数据源 {id} {state}', actionExecuting: '正在执行动作: {id}...', actionSuccess: '执行成功', @@ -1170,6 +1268,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { shutdownSettingsSaved: '关机设置已保存', defaultsRestored: '已恢复默认设置', restoreFailed: '恢复失败', + restoreFailedMsg: '恢复失败: {msg}', deleteRuleResult: '删除规则 {id}', deleteRuleFailed: '删除规则失败', getRuleDetailFailed: '获取规则详情失败', @@ -1178,7 +1277,12 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { createActionFirst: '请先创建动作模板', ruleCreated: '规则 {id} {action}成功', ruleCreateFailed: '{action}规则失败', + actionResult: '{action}: {msg}', + actionFailedMsg: '{action} 失败: {msg}', + ruleUpdate: '更新', + ruleCreate: '创建', configExported: '已导出{type}配置: {filename}', + ruleConfigExported: '已导出规则配置: {filename}', selectFile: '请先选择文件', configImported: '已导入配置,重启后生效', // 表单验证 @@ -1243,14 +1347,32 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { // 网络 wifiModeChanged: 'WiFi 模式已切换为 {mode}', // 文件操作 + createDirFailed: '创建目录失败: {msg}', + enterQrContent: '请输入 QR 码内容', + enterDisplayText: '请输入显示文本', + textStopped: '文本已停止', + selectFileToDelete: '请先选择要删除的文件', + selectFileToDownload: '请先选择要下载的文件', noDownloadableFiles: '选中的项目中没有可下载的文件(文件夹不支持下载)', + batchDownloadComplete: '批量下载完成', + deletingItems: '正在删除 {count} 个项目...', + deleteSuccessCount: '成功删除 {count} 个项目', + deletePartial: '删除完成: {success} 成功, {fail} 失败', + selectFileToUpload: '请选择要上传的文件', + uploadComplete: '上传完成', downloadingFiles: '正在下载 {count} 个文件...', sdCardUnmounted: 'SD 卡已卸载', unmountFailed: '卸载失败', enterFolderName: '请输入文件夹名称', folderCreated: '文件夹创建成功', + createFailedMsg: '创建失败: {msg}', enterNewName: '请输入新名称', renameSuccess: '重命名成功', + renameFailedMsg: '重命名失败: {msg}', + downloadStart: '下载开始', + downloadFailedMsg: '下载失败: {msg}', + deleteSuccess: '删除成功', + deleteFailedMsg: '删除失败: {msg}', // SSH 页面 selectHostFirst: '请先选择一个主机', fillCommandNameAndCmd: '请填写指令名称和命令', @@ -1270,17 +1392,21 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { processAlreadyStopped: '进程已经停止', serviceNotRunning: '服务未运行', stopServiceFailed: '停止服务失败', + stopServiceFailedMsg: '停止服务失败: {msg}', commandRunning: '有命令正在执行中,请先取消或等待完成', startExecFailed: '启动执行失败', + startExecFailedMsg: '启动执行失败: {msg}', noRunningCommand: '没有正在执行的命令', cancelSent: '取消请求已发送', cancelFailed: '取消失败', + cancelFailedMsg: '取消失败: {msg}', patternMatchSuccess: '模式匹配成功', commandSuccess: '命令执行成功', commandMatchFailed: '命令执行完成,模式匹配失败', commandTimeout: '命令执行超时', commandCompletedCode: '命令执行完成,退出码: {code}', execError: '执行出错', + execErrorMsg: '执行出错: {msg}', commandCancelled: '命令已取消', hostFingerprintDeleted: '已删除主机指纹', hostInfoEmpty: '主机信息为空', @@ -1315,6 +1441,8 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { selectedJsonFiles: '已选择当前目录下的所有 JSON 文件', configPackCopied: '配置包已复制到剪贴板', configPackDownloaded: '配置包已下载: {filename}', + configPackVerifySuccess: '配置包验证成功\n签名者: {signer} {isOfficial}', + configPackVerifyFailed: '配置包验证失败: {msg}', // OTA otaUpgradeFailed: '升级失败', webuiUpgradeFailed: 'WebUI 升级启动失败', @@ -1539,8 +1667,10 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { statusFailed: '失败', statusIdle: '未启动', statusStopped: '已停止', + statusUnknown: '未知', // 错误消息 hostNotFound: '主机信息不存在', + hostNotExistShort: '主机不存在', cmdNotFound: '命令不存在', // 匹配结果标签 matchResultTitle: '匹配结果', @@ -1571,9 +1701,12 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { selectDevice: '-- 选择设备 --', noKeysHint: '暂无密钥,点击上方按钮生成新密钥', noDeployedHint: '暂无已部署主机,请先在上方密钥管理中点击「部署」', + noDeployedHostsMsg: '暂无已部署主机', + deployKeyAtSecurityHint: '请先到 安全 页面部署 SSH 公钥', noKnownHosts: '暂无已知主机指纹', pleaseConfigSshHost: '-- 请先配置 SSH 主机 --', noVariableData: '该指令暂无变量数据,请先执行一次', + getVarFailed: '获取变量失败', // 变量表格表头 varTableName: '变量名', varTableType: '类型', @@ -1625,9 +1758,17 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { stopServiceFailed: '停止服务失败', executionFailed: '执行失败', getLogFailed: '获取日志失败', + getLogFailedMsg: '获取日志失败: {msg}', + startRealTimeTail: '开始实时跟踪: {logFile}\n(点击"停止跟踪"按钮退出)', + viewServiceLogFile: '查看服务日志: {name}\n文件: {file}', + logErrorMsg: '[错误] {msg}', + stopServiceName: '停止服务: {name}', + startExecFailedDetail: '启动执行失败\n\n{msg}', patternMatchSuccess: '模式匹配成功!', expectPatternMatch: '期望模式匹配: 是', failPatternMatch: '失败模式匹配: 是', + failMatchedTrue: 'true (检测到错误)', + failMatchedFalse: 'false', extractedContent: '提取内容', cancelledExecution: '已取消执行', error: '错误', @@ -2172,6 +2313,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { // OTA 状态 stateIdle: '空闲', stateChecking: '检查更新中...', + downloading: '下载中...', downloadingWebUI: '下载 WebUI...', downloadingFirmware: '下载固件...', stateVerifying: '验证中...', @@ -2209,7 +2351,13 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { refreshPage: '刷新页面', currentVersionUnknown: '未知', step1: '[1/2] 升级固件', - step2: '[2/2] 升级 WebUI' + step2: '[2/2] 升级 WebUI', + step1Connecting: '[1/2] 正在连接服务器...', + preparing: '准备中...', + writing: '写入中...', + readingFile: '正在读取文件...', + stateError: '错误', + connecting: '正在连接...' }, // 自动化页面 @@ -2260,7 +2408,12 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { requestBodyLabel: '请求体', requestBodyHint: 'JSON 格式,支持变量', getMemoryDetailFailed: '获取内存详情失败', - // 日志 + // 日志(快捷操作日志跟踪) + logFetchFailed: '[获取失败]', + logError: '[错误]', + deviceBusyRetry: '若设备繁忙可稍后重试。', + logEmptyBracket: '[空]', + logFileEmptyBracket: '[日志文件不存在或为空]', logFileNotExist: '日志文件不存在或为空', logEmpty: '暂无日志', noRules: '暂无规则,点击"添加"创建第一条', @@ -2324,6 +2477,10 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { logLevel: '级别', logMessage: '消息', logMsgHint: '支持变量: ${变量名}', + varConfig: '变量配置', + varNameLabel: '变量名', + method: '方法', + requestBody: '请求体', indexPlaceholder: '索引', pulseMsPlaceholder: '脉冲ms', varNamePlaceholder: '变量名', @@ -2377,6 +2534,12 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { stopTracking: '停止跟踪', startTracking: '▶️ 开始跟踪', interval: '间隔', + interval1Sec: '1秒', + interval2Sec: '2秒', + interval3Sec: '3秒', + interval5Sec: '5秒', + interval10Sec: '10秒', + interval30Sec: '30秒', realTimeUpdating: '● 实时更新中', trackingStopped: '已停止更新', // 动作编辑器标签 @@ -2439,6 +2602,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { ledOffHint: '关闭 LED 设备,无需额外参数', filterStopHint: '停止当前运行的滤镜效果,无需额外参数', textStopHint: '停止当前运行的文本覆盖层,无需额外参数', + textContentLabel: '文本内容', textPlaceholder: '要显示的文本,支持 ${变量名}', scroll: '滚动', scrollNone: '无滚动', @@ -2670,6 +2834,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { secondaryExpressionPlaceholder: '例如: ${max_value}', displayLines: '显示行数', refreshIntervalMs: '刷新间隔(毫秒)', + configLogVariableFirst: '请先配置日志变量', logVariable: '日志变量', logVariablePlaceholder: '选择包含日志文本的变量', selectVariable: '选择变量', @@ -2745,6 +2910,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { // Modal - Effect effectTitle: '程序动画', effectNotSelected: '未选择', + current: '当前', speed: '速度', color: '颜色', start: '启动', @@ -2892,17 +3058,30 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { remainingDays: '剩余 {days} 天', generatingKeyPair: '正在生成密钥对...', generatingCsr: '正在生成 CSR...', + ecdsaKeyPairSuccess: 'ECDSA P-256 密钥对生成成功!', + installingCert: '正在安装证书...', + certInstalledSuccess: '证书安装成功!', installFailed: '安装失败', + installFailedMsg: '安装失败: {msg}', + deployFailedMsg: '部署失败: {msg}', + revokeFailedMsg: '撤销失败: {msg}', + invalidJsonConfigMsg: '配置文件不是有效的 JSON: {msg}', // 配置包 enterConfigName: '请输入配置名称', selectConfigFile: '请选择配置文件', pasteTargetCert: '请粘贴目标设备证书', invalidJsonConfig: '配置文件不是有效的 JSON', generatingPack: '生成配置包中', + generatingPackWithCount: '生成配置包中 ({count} 个文件)...', savedToDevice: '已保存到设备', + installingCaCert: '正在安装 CA 证书链...', + caCertInstalledSuccess: 'CA 证书链安装成功!', + loadFailedMsg: '加载失败: {msg}', generationFailed: '生成失败', + generationFailedMsg: '生成失败: {msg}', verifying: '验证中...', verifyFailed: '验证失败', + signatureInvalid: '签名无效', importing: '导入中...', importFailed: '导入失败', uploadOrPasteContent: '请上传文件或粘贴配置包内容' @@ -2931,6 +3110,8 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { turnOn: '开启', colorSettings: '颜色设置', turnOffLight: '关灯', + turnOnLight: '开灯', + noFilterSelected: '未选择滤镜', // OTA firmwareUpgradeComplete: '固件升级完成,准备升级 WebUI...', allUpgradeComplete: '全部升级完成', @@ -2961,6 +3142,7 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { newCommand: '+ 新建指令', editCommand: '编辑指令', commandVariables: '指令变量', + commandVariablesWithName: '指令变量: {varName}.*', sourceVariables: '{source} 变量', editActionTemplate: '编辑动作模板', // 动作 @@ -3049,6 +3231,15 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { confirmRemoveKnownHost: '确定要移除主机 "{host}:{port}" 的记录吗?', confirmClearKnownHosts: '确定要清除所有已知主机记录吗?此操作不可撤销!', confirmDeletePKI: '确定要删除所有 PKI 凭证吗?\n\n这将删除:\n• 私钥\n• 设备证书\n• CA 证书链\n\n此操作不可撤销!', + confirmMarkFirmwareValid: '确认将当前固件标记为有效?\n这将取消自动回滚保护。', + confirmRollback: '确认回滚到上一版本固件?\n\n系统将立即重启并加载上一个分区的固件。\n请确保上一版本固件可用!', + confirmAbortUpgrade: '确认中止当前升级?', + confirmRestoreDefaults: '确认恢复默认设置?', + confirmDeleteFiles: '确定要删除选中的 {count} 个文件/文件夹吗?此操作不可撤销!', + confirmDeleteItem: '确定要删除 "{name}" 吗?', + confirmDeleteAction: '确定要删除动作模板 "{id}" 吗?', + confirmDeleteSource: '确定要删除数据源 "{id}" 吗?此操作不可撤销。', + confirmDeleteRule: '确定要删除规则 "{id}" 吗?此操作不可撤销。', // Alert 对话框和其他提示 alertHostFingerprint: '主机: {host}:{port}\n类型: {type}\n指纹 (SHA256):\n{fingerprint}', alertEnterWsAddress: '请输入 WebSocket 地址', @@ -3092,7 +3283,29 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', { connecting: '正在连接到设备...', connected: '已连接到设备', helpHint: '输入 {help} 查看可用命令', + connectionDisconnected: '连接已断开', + reconnectIn: '5秒后尝试重新连接...', + reconnecting: '正在重新连接...', + connectionError: '连接错误', + notConnected: '未连接到设备', + sshShellRequiresParams: '错误: SSH shell 需要 --host 和 --user 参数', + sshSessionInProgress: 'SSH 会话已在进行中', + sshConnectingTo: '正在连接到 {user}@{host}:{port}...', + exitSshHint: '(按 Ctrl+\\ 退出 SSH shell)', + exitSshDisplay: '^\\ (退出 SSH shell)', + lowVoltageWarning: '⚠️ 低电压警告! 电压: {voltage}V - 开始关机倒计时', + countdownTick: '⏱️ 关机倒计时: {countdown}秒 | 电压: {voltage}V', + shutdownStart: '🔴 正在执行关机... 电压: {voltage}V', + protected: '🛡️ 进入保护状态 | 等待电压恢复...', + recoveryStart: '🔄 电压恢复中: {voltage}V | 等待稳定...', + recoveryComplete: '✅ 电压恢复完成! {voltage}V | 系统即将重启', + debugTick: '📊 [调试] {state} | 电压: {voltage}V | 倒计时: {countdown}s', + voltageNormal: '✓ 电压状态正常: {voltage}V', + stateChanged: '状态变更: {state} | 电压: {voltage}V', + powerEvent: '[电源] {event}: 状态={state}, 电压={voltage}V', pageTitle: 'Web 终端', + errorLabel: '错误', + unknownError: '未知错误', terminalHint: '提示: 输入 help 查看命令 | Ctrl+C 中断 | Ctrl+L 清屏 | ↑↓ 历史', systemLogTitle: '系统日志', levelLabel: '级别', diff --git a/components/ts_webui/web/js/terminal.js b/components/ts_webui/web/js/terminal.js index 8bf9134..547e480 100644 --- a/components/ts_webui/web/js/terminal.js +++ b/components/ts_webui/web/js/terminal.js @@ -147,7 +147,7 @@ class WebTerminal { if (this.sshMode) { // 检查 Ctrl+\ (0x1C) 退出 SSH if (data.charCodeAt(0) === 0x1C) { - this.writeln('\r\n^\\ (退出 SSH shell)'); + this.writeln('\r\n' + (typeof t === 'function' ? t('terminal.exitSshDisplay') : '^\\ (退出 SSH shell)')); this.exitSshShell(); return; } @@ -335,14 +335,14 @@ class WebTerminal { this.pingInterval = null; } - this.writeln('\r\n\x1b[1;31m连接已断开\x1b[0m'); + this.writeln('\r\n\x1b[1;31m' + (typeof t === 'function' ? t('terminal.connectionDisconnected') : '连接已断开') + '\x1b[0m'); // 尝试重连 if (event.code !== 1000) { // 非正常关闭 - this.writeln('\x1b[33m5秒后尝试重新连接...\x1b[0m'); + this.writeln('\x1b[33m' + (typeof t === 'function' ? t('terminal.reconnectIn') : '5秒后尝试重新连接...') + '\x1b[0m'); setTimeout(() => { if (!this.connected) { - this.writeln('正在重新连接...'); + this.writeln(typeof t === 'function' ? t('terminal.reconnecting') : '正在重新连接...'); this.connect(); } }, 5000); @@ -351,7 +351,7 @@ class WebTerminal { this.ws.onerror = (error) => { console.error('Terminal WebSocket error:', error); - this.writeln('\r\n\x1b[1;31m连接错误\x1b[0m'); + this.writeln('\r\n\x1b[1;31m' + (typeof t === 'function' ? t('terminal.connectionError') : '连接错误') + '\x1b[0m'); }; } @@ -382,7 +382,7 @@ class WebTerminal { break; case 'error': - this.writeln('\x1b[1;31m错误: ' + (msg.message || '未知错误') + '\x1b[0m'); + this.writeln('\x1b[1;31m' + (typeof t === 'function' ? t('terminal.errorLabel') : '错误') + ': ' + (msg.message || (typeof t === 'function' ? t('terminal.unknownError') : '未知错误')) + '\x1b[0m'); this.writePrompt(); break; @@ -423,10 +423,11 @@ class WebTerminal { let notification = ''; let color = '\x1b[33m'; // 默认黄色 + const _t = (key, params, fallback) => (typeof t === 'function' ? t(key, params) : fallback); switch (event) { case 'low_voltage': color = '\x1b[1;31m'; // 亮红色 - notification = `⚠️ 低电压警告! 电压: ${voltage}V - 开始关机倒计时`; + notification = _t('terminal.lowVoltageWarning', { voltage }, `⚠️ 低电压警告! 电压: ${voltage}V - 开始关机倒计时`); break; case 'countdown_tick': if (countdown <= 10) { @@ -436,39 +437,39 @@ class WebTerminal { } else { return; // 不显示每秒倒计时,只显示关键时刻 } - notification = `⏱️ 关机倒计时: ${countdown}秒 | 电压: ${voltage}V`; + notification = _t('terminal.countdownTick', { countdown, voltage }, `⏱️ 关机倒计时: ${countdown}秒 | 电压: ${voltage}V`); break; case 'shutdown_start': color = '\x1b[1;31m'; - notification = `🔴 正在执行关机... 电压: ${voltage}V`; + notification = _t('terminal.shutdownStart', { voltage }, `🔴 正在执行关机... 电压: ${voltage}V`); break; case 'protected': color = '\x1b[35m'; // 紫色 - notification = `🛡️ 进入保护状态 | 等待电压恢复...`; + notification = _t('terminal.protected', {}, '🛡️ 进入保护状态 | 等待电压恢复...'); break; case 'recovery_start': color = '\x1b[36m'; // 青色 - notification = `🔄 电压恢复中: ${voltage}V | 等待稳定...`; + notification = _t('terminal.recoveryStart', { voltage }, `🔄 电压恢复中: ${voltage}V | 等待稳定...`); break; case 'recovery_complete': color = '\x1b[1;32m'; // 亮绿色 - notification = `✅ 电压恢复完成! ${voltage}V | 系统即将重启`; + notification = _t('terminal.recoveryComplete', { voltage }, `✅ 电压恢复完成! ${voltage}V | 系统即将重启`); break; case 'debug_tick': // 调试模式:每秒显示状态 color = '\x1b[36m'; // 青色 - notification = `📊 [调试] ${state} | 电压: ${voltage}V | 倒计时: ${countdown}s`; + notification = _t('terminal.debugTick', { state, voltage, countdown }, `📊 [调试] ${state} | 电压: ${voltage}V | 倒计时: ${countdown}s`); break; case 'state_changed': if (state === 'NORMAL') { color = '\x1b[32m'; - notification = `✓ 电压状态正常: ${voltage}V`; + notification = _t('terminal.voltageNormal', { voltage }, `✓ 电压状态正常: ${voltage}V`); } else { - notification = `状态变更: ${state} | 电压: ${voltage}V`; + notification = _t('terminal.stateChanged', { state, voltage }, `状态变更: ${state} | 电压: ${voltage}V`); } break; default: - notification = `[电源] ${event}: 状态=${state}, 电压=${voltage}V`; + notification = _t('terminal.powerEvent', { event, state, voltage }, `[电源] ${event}: 状态=${state}, 电压=${voltage}V`); } if (notification) { @@ -504,7 +505,7 @@ class WebTerminal { data: command })); } else { - this.writeln('\x1b[1;31m未连接到设备\x1b[0m'); + this.writeln('\x1b[1;31m' + (typeof t === 'function' ? t('terminal.notConnected') : '未连接到设备') + '\x1b[0m'); this.writePrompt(); } } @@ -527,7 +528,7 @@ class WebTerminal { const portMatch = argsStr.match(/--port\s+(\d+)/i); if (!hostMatch || !userMatch) { - this.writeln('\x1b[1;31m错误: SSH shell 需要 --host 和 --user 参数\x1b[0m'); + this.writeln('\x1b[1;31m' + (typeof t === 'function' ? t('terminal.sshShellRequiresParams') : '错误: SSH shell 需要 --host 和 --user 参数') + '\x1b[0m'); this.writePrompt(); return true; } @@ -548,13 +549,14 @@ class WebTerminal { */ startSshShell(params) { if (this.sshConnecting || this.sshMode) { - this.writeln('\x1b[1;31mSSH 会话已在进行中\x1b[0m'); + this.writeln('\x1b[1;31m' + (typeof t === 'function' ? t('terminal.sshSessionInProgress') : 'SSH 会话已在进行中') + '\x1b[0m'); this.writePrompt(); return; } this.sshConnecting = true; - this.writeln(`\x1b[36m正在连接到 ${params.user}@${params.host}:${params.port}...\x1b[0m`); + const connMsg = typeof t === 'function' ? t('terminal.sshConnectingTo', { user: params.user, host: params.host, port: params.port }) : `正在连接到 ${params.user}@${params.host}:${params.port}...`; + this.writeln(`\x1b[36m${connMsg}\x1b[0m`); if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ @@ -594,7 +596,7 @@ class WebTerminal { this.sshMode = true; this.sshConnecting = false; this.writeln(`\x1b[1;32m${message}\x1b[0m`); - this.writeln('\x1b[90m(按 Ctrl+\\ 退出 SSH shell)\x1b[0m'); + this.writeln('\x1b[90m' + (typeof t === 'function' ? t('terminal.exitSshHint') : '(按 Ctrl+\\ 退出 SSH shell)') + '\x1b[0m'); this.writeln(''); break; case 'closed': diff --git a/docs/I18N_REMAINING_CHECK.md b/docs/I18N_REMAINING_CHECK.md new file mode 100644 index 0000000..5522c1e --- /dev/null +++ b/docs/I18N_REMAINING_CHECK.md @@ -0,0 +1,17 @@ +# i18n 收尾检查报告 + +> 根据 `/Users/massif/Desktop/plan.md` 对项目逐项核查,所有剩余项已于 2025-02-12 完成 i18n 接入。 + +## 已完成的修改概览 + +| 类别 | 修改内容 | +|------|----------| +| 语言包 | 新增 common.loadFailedMsg、refreshOnce、uploadFailedMsg;toast.terminate*;terminal.errorLabel、unknownError;sshPage.getVarFailed、failMatchedTrue/False | +| app.js | terminateProcess msgMap、数据组件工具栏/状态、错误文案、变量分组、图标预览、取消/密钥选择、配置包验证、匹配结果等 | +| terminal.js | 错误输出文案 | +| index.html | 首屏 Loading 增加 data-i18n | + +## 参考 + +- 计划文档:`/Users/massif/Desktop/plan.md` +- 语言包:`components/ts_webui/web/js/lang/zh-CN.js`、`en-US.js`