Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions components/ts_api/src/ts_api_system.c
Original file line number Diff line number Diff line change
Expand Up @@ -697,15 +697,15 @@ static esp_err_t api_system_memory_detail(const cJSON *params, ts_api_result_t *
if (dram_free > 0) {
float frag = 100.0f * (1.0f - (float)dram_largest / (float)dram_free);
if (frag > 60) {
cJSON_AddItemToArray(tips, cJSON_CreateString("warning:DRAM 碎片化严重,建议重启系统"));
cJSON_AddItemToArray(tips, cJSON_CreateString("warning:dram_fragmented"));
}
}
}

if (psram_total > 0) {
int psram_used_pct = 100 * (psram_total - psram_free) / psram_total;
if (psram_used_pct < 50) {
cJSON_AddItemToArray(tips, cJSON_CreateString("info:PSRAM 空间充足,可用于大型缓冲区"));
cJSON_AddItemToArray(tips, cJSON_CreateString("info:psram_sufficient"));
}
}

Expand Down
52 changes: 40 additions & 12 deletions components/ts_webui/web/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ body {
.nav {
display: flex;
gap: 20px;
margin-left: 40px;
margin-left: 0;
}

.nav-link {
Expand Down Expand Up @@ -95,21 +95,21 @@ body {
padding: 20px;
}

/* Footer */
/* Footer:行高=高度,单行文字在 50px 内由行框自然垂直居中,不依赖像素微调 */
.footer {
height: var(--footer-height);
line-height: var(--footer-height);
background: transparent;
border-top: none;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-light);
font-size: 0.9rem;
}

.footer p {
margin: 0;
line-height: 1;
display: inline;
line-height: inherit;
}

/* Cards */
Expand Down Expand Up @@ -984,24 +984,32 @@ button.btn-gray:hover,
/* 新增样式 */
/* ============================================================ */

/* WebSocket 状态指示器 */
/* WebSocket 状态指示器(位于用户菜单内、root 图标左侧,与 user-menu 的 gap 保持间隔) */
.ws-status {
width: 12px;
height: 12px;
border-radius: 50%;
background: #e74c3c;
margin-left: 10px;
flex-shrink: 0;
transition: background 0.3s;
}

.ws-status.connected {
background: #2e7d32;
}

/* Header separator line */
.header-separator {
width: 1px;
height: 24px;
background-color: #d0d0d0;
margin: 0 16px;
}

/* 语言切换按钮 */
.lang-switch {
margin-left: 12px;
margin-right: 8px;
margin-left: 16px;
margin-right: 0;
position: relative;
}

Expand All @@ -1024,6 +1032,18 @@ button.btn-gray:hover,
border-color: var(--primary-color);
}

/* 语言切换按钮:无底色、无外框 */
.lang-btn.btn-service-style {
background: transparent !important;
color: #007bff !important;
border: none !important;
}
.lang-btn.btn-service-style:hover {
background: transparent !important;
color: #007bff !important;
border: none !important;
}

#lang-icon {
font-size: 1rem;
}
Expand Down Expand Up @@ -3061,6 +3081,12 @@ button.btn-gray:hover,
padding: 5px 12px;
font-size: 0.85rem;
}
/* 安全页按钮 icon 与文字间距统一(与其他页面一致) */
.page-security .btn {
display: inline-flex;
align-items: center;
gap: 6px;
}
/* HTTPS 证书一行:CSR / 证书 / 删除 等按钮统一宽度,视觉一致(含表格行内按钮) */
.page-security .button-group .btn {
min-width: 6rem;
Expand All @@ -3077,6 +3103,7 @@ button.btn-gray:hover,
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
box-sizing: border-box;
text-align: center;
}
Expand Down Expand Up @@ -5382,7 +5409,7 @@ button.btn-gray:hover,

/* 条件逻辑缩短,避免与冷却时间、立即启用挤在一起 */
#add-rule-modal .form-row.three-col .form-group-logic {
flex: 0 0 90px;
flex: 0 0 120px;
min-width: 0;
}

Expand Down Expand Up @@ -7018,7 +7045,8 @@ button.btn-gray:hover,
}

.data-widgets-empty p {
margin-bottom: 0;
margin: 0 0 4px;
font-size: 0.95em;
}

/* 组件卡片 */
Expand Down
128 changes: 110 additions & 18 deletions components/ts_webui/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@
<img src="/images/tslogo-48.png" alt="TianshanOS Logo" class="logo-icon">
<span class="logo-text">TianshanOS</span>
</div>
<div class="lang-switch" id="lang-switch" title="切换语言 / Switch Language">
<button class="btn btn-sm lang-btn btn-service-style" onclick="toggleLanguageMenu()">
<span id="lang-name">中文</span>
<span class="lang-arrow">▼</span>
</button>
<div id="lang-menu" class="lang-menu hidden">
<!-- 动态填充语言列表 -->
</div>
</div>
<div class="header-separator"></div>
<nav class="nav">
<a href="#/" class="nav-link active" data-i18n="nav.system">系统</a>
<a href="#/network" class="nav-link" data-i18n="nav.network">网络</a>
Expand All @@ -28,19 +38,10 @@
<a href="#/commands" class="nav-link" data-requires-root data-i18n="ssh.commands">指令</a>
<a href="#/security" class="nav-link" data-i18n="nav.security">安全</a>
</nav>
<div class="ws-status" id="ws-status" data-i18n-title="network.connected"></div>
<div class="lang-switch" id="lang-switch" title="切换语言 / Switch Language">
<button class="btn btn-sm lang-btn" onclick="toggleLanguageMenu()">
<span id="lang-name">中文</span>
<span class="lang-arrow">▼</span>
</button>
<div id="lang-menu" class="lang-menu hidden">
<!-- 动态填充语言列表 -->
</div>
</div>
<div class="user-menu">
<div class="ws-status" id="ws-status" data-i18n-title="network.connected"></div>
<span id="user-name" data-i18n="ui.notLoggedIn">未登录</span>
<button id="login-btn" class="btn btn-primary" data-i18n="security.login">登录</button>
<button id="login-btn" class="btn btn-service-style" data-i18n="security.login">登录</button>
</div>
</header>

Expand All @@ -49,7 +50,7 @@
<!-- 动态页面内容由 SPA 路由器加载 -->
<div class="loading">
<i class="ri-refresh-line loading-icon"></i>
<p data-i18n="common.loading">加载中...</p>
<p data-i18n="common.loading">Loading...</p>
</div>
</div>
</main>
Expand Down Expand Up @@ -81,7 +82,7 @@ <h2><i class="ri-door-open-line"></i> <span data-i18n="login.welcome">登录 Tia
</div>
<div class="modal-footer">
<button type="button" class="btn" onclick="closeLoginModal()" data-i18n="common.cancel">取消</button>
<button type="submit" class="btn btn-primary" data-i18n="login.loginButton">登录</button>
<button type="submit" class="btn btn-service-style" data-i18n="login.loginButton">登录</button>
</div>
</form>
</div>
Expand Down Expand Up @@ -113,7 +114,7 @@ <h2><i class="ri-folder-open-line"></i> <span data-i18n="files.selectFiles">选
<span id="file-picker-current-path">/sdcard/images</span>
</div>
<div class="file-picker-list" id="file-picker-list">
<div class="loading" data-i18n="common.loading">加载中...</div>
<div class="loading" data-i18n="common.loading">Loading...</div>
</div>
<div class="file-picker-selected">
<span data-i18n="files.fileName">已选择</span>: <span id="file-picker-selected-name">-</span>
Expand All @@ -133,7 +134,7 @@ <h2><span data-i18n="system.memoryDetail">内存详细分析</span></h2>
<button class="modal-close" onclick="hideMemoryDetailModal()"><i class="ri-close-line"></i></button>
</div>
<div class="modal-body" id="memory-detail-body">
<div class="loading" data-i18n="common.loading">加载中...</div>
<div class="loading" data-i18n="common.loading">Loading...</div>
</div>
<div class="modal-footer" style="margin-top:16px;padding-top:12px;border-top:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center">
<span id="memory-detail-timestamp" style="font-size:0.8em;color:#888">-</span>
Expand All @@ -150,7 +151,7 @@ <h2><i class="ri-bar-chart-box-line"></i> <span data-i18n="automation.viewVariab
<button class="modal-close" onclick="closeSourceVariablesModal()"><i class="ri-close-line"></i></button>
</div>
<div class="modal-body" id="source-variables-body">
<div class="loading" data-i18n="common.loading">加载中...</div>
<div class="loading" data-i18n="common.loading">Loading...</div>
</div>
<div class="modal-footer cc-compact-footer">
<button class="btn" onclick="closeSourceVariablesModal()" style="color:#666" data-i18n="common.close">关闭</button>
Expand Down Expand Up @@ -205,8 +206,99 @@ <h2><span data-i18n="system.shutdownSettings">电压保护设置</span></h2>
</div>
</div>

<!-- i18n 国际化 -->
<script src="/js/i18n.js"></script>
<!-- i18n 国际化(内联避免 ERR_CONNECTION_RESET 导致 i18n 未定义) -->
<script>
(function(){
var i18n = (function() {
var currentLang = 'zh-CN';
var languages = {};
var supportedLanguages = { 'zh-CN': { name: '简体中文', flag: '' }, 'en-US': { name: 'English', flag: '' } };
function registerLanguage(code, translations) { languages[code] = translations; }
function setLanguage(lang) {
if (languages[lang]) {
currentLang = lang;
try { localStorage.setItem('ts_language', lang); } catch(e) {}
document.documentElement.lang = lang;
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: lang } }));
return true;
}
return false;
}
function getLanguage() { return currentLang; }
function translate(key, params) {
params = params || {};
var translations = languages[currentLang] || languages['zh-CN'] || {};
var value = key.split('.').reduce(function(obj, k) { return obj && obj[k]; }, translations);
if (value === undefined && currentLang !== 'en-US' && languages['en-US'])
value = key.split('.').reduce(function(obj, k) { return obj && obj[k]; }, languages['en-US']);
if (value === undefined) return key;
if (typeof value === 'string' && Object.keys(params).length > 0)
value = value.replace(/\{(\w+)\}/g, function(m, p) { return params[p] !== undefined ? params[p] : m; });
return value;
}
function init() {
try {
var saved = localStorage.getItem('ts_language');
if (saved && languages[saved]) currentLang = saved;
else currentLang = (navigator.language || '').indexOf('zh') === 0 ? 'zh-CN' : 'en-US';
} catch(e) {}
document.documentElement.lang = currentLang;
}
function translateDOM(root) {
root = root || document;
root.querySelectorAll('[data-i18n]').forEach(function(el) {
var key = el.getAttribute('data-i18n');
var params = el.dataset.i18nParams ? JSON.parse(el.dataset.i18nParams) : {};
el.textContent = translate(key, params);
});
root.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) { el.placeholder = translate(el.getAttribute('data-i18n-placeholder')); });
root.querySelectorAll('[data-i18n-title]').forEach(function(el) { el.title = translate(el.getAttribute('data-i18n-title')); });
}
return { registerLanguage: registerLanguage, setLanguage: setLanguage, getLanguage: getLanguage, translate: translate, init: init, getSupportedLanguages: function() { return supportedLanguages; }, translateDOM: translateDOM };
})();
window.i18n = i18n;
window.t = i18n.translate;
window.setLanguage = i18n.setLanguage;
window.getLanguage = i18n.getLanguage;
window.translateDOM = i18n.translateDOM;
window.toggleLanguageMenu = function() {
var menu = document.getElementById('lang-menu');
if (!menu) return;
if (menu.classList.contains('hidden')) {
var langs = i18n.getSupportedLanguages(), cur = i18n.getLanguage();
menu.innerHTML = Object.keys(langs).map(function(code) {
var info = langs[code];
return '<div class="lang-menu-item' + (code === cur ? ' active' : '') + '" onclick="selectLanguage(\'' + code + '\')"><span class="lang-menu-name">' + info.name + '</span>' + (code === cur ? '<span class="lang-menu-check">✓</span>' : '') + '</div>';
}).join('');
menu.classList.remove('hidden');
setTimeout(function() { document.addEventListener('click', window.closeLangMenuOnClickOutside); }, 10);
} else menu.classList.add('hidden');
};
window.closeLangMenuOnClickOutside = function(e) {
var sw = document.getElementById('lang-switch');
if (sw && !sw.contains(e.target)) {
var m = document.getElementById('lang-menu');
if (m) m.classList.add('hidden');
document.removeEventListener('click', window.closeLangMenuOnClickOutside);
}
};
window.selectLanguage = function(lang) {
if (i18n.setLanguage(lang)) {
var langs = i18n.getSupportedLanguages(), nameEl = document.getElementById('lang-name');
if (nameEl && langs[lang]) nameEl.textContent = langs[lang].name.split(' ')[0];
i18n.translateDOM();
var m = document.getElementById('lang-menu');
if (m) m.classList.add('hidden');
}
};
document.addEventListener('DOMContentLoaded', function() {
i18n.init();
var langs = i18n.getSupportedLanguages(), cur = i18n.getLanguage(), nameEl = document.getElementById('lang-name');
if (nameEl && langs[cur]) nameEl.textContent = cur === 'zh-CN' ? '中文' : 'EN';
i18n.translateDOM();
});
})();
</script>
<script src="/js/lang/zh-CN.js"></script>
<script src="/js/lang/en-US.js"></script>

Expand Down
Loading