feat(panel, PANEL_ENHANCE_PLAN 批2): 崩溃/日志中心 + 音频写 + 后端补缺口
后端(meowpanel.c 0-warning,-Werror 通过): - /api/system 补 reboot_reason/upgrade_status(svc_power 读级 POWER_REBOOT_REASON(8)/UPGRADE_STATUS(9)) - /api/overview net 块并入 DNS 统计(首页严格单请求;DNS 不可用省略 dns 键不谎报) - /api/audio 补 vol_max(读 settings audio.volume_max) - POST /api/audio/route(仅 0=auto/2=3.5/3=4.4,拒 1/4/5;写级 PIN 已护) - POST /api/audio/volume(clamp 到 vol_max,防炸耳红线;写级 PIN 已护) - GET /api/logs/crash(崩溃汇总:每实体崩溃数+最近时间/信号 + dmesg 计数;256KB/64 实体/5 行上限) - /api/logs 文件列表加 group 来源分组 - 顺带补回 批1-B 前端读取的 boot.mode /tmp/meow_boot_mode(此前遗留未提交) 前端(web/meowpanel.js 双主题): - 音频页:路由选择器(自动/3.5/4.4)+ 音量滑条(上限 vol_max、平方锥度非线性标注、确认弹窗) - 日志页:崩溃汇总卡 + 崩溃尾巴一键看 stdout 尾部 + 按来源分组 + 前缀过滤 - 首页:netCard 改读 overview net.dns,去掉单独 /api/net/dns 验证:宿主编译 0 warning + -Werror;自建桩测试 24/24(crash 解析/路由 1|4|5 拒 422/音量 clamp 到 50/ reboot_reason+upgrade_status 值/overview net.dns);headless Chromium 渲染 audio/logs/home/system/ light 主题均正确。md5(meowpanel)=aed2ea17a34614acaa26dcbc8965ebb1。
This commit is contained in:
1 parent
34f2834704
commit
33d3059038
5 files changed
+547
-59
No files matched your search
@@ -503,6 +503,7 @@
|
||||
* 电平:rms(青绿) + peak(黄) 双条 dBFS 表(-60..0 刻度)+ deci-dB 数值 + 近 30 采样 sparkline(客户端自累积)。
|
||||
* 短流时显示「无流」;audiod 不在 → available:false。 */
|
||||
let audioTimer = null;
|
||||
let audioVolDrag = false; // 音量滑条拖拽中(暂停 1s 重渲染,防打断交互)
|
||||
function stopAudio() { if (audioTimer) { clearInterval(audioTimer); audioTimer = null; } }
|
||||
function audioPortName(p) { // audiod numid=9 路由口语义(AUDIOD_DESIGN §5)
|
||||
const m = { 0: '空闲', 1: 'Line Out', 2: '3.5mm', 3: '4.4mm 平衡', 4: 'Type-C SPDIF' };
|
||||
@@ -510,6 +511,35 @@
|
||||
return m[p] || ('端口 ' + p);
|
||||
}
|
||||
function fmtFrames(n) { return (n == null) ? '—' : Number(n).toLocaleString(); }
|
||||
/* 路由口 → 中文(写选择器用;0=auto/2=3.5/3=4.4,绝不暴露 4=SPDIF/5=audiod 内部值)。 */
|
||||
function audioRouteLabel(p) {
|
||||
const m = { 0: '自动', 2: '3.5mm', 3: '4.4mm' };
|
||||
return m[p] || ('端口 ' + p);
|
||||
}
|
||||
/* 路由与音量写控件(批2):路由分段选择 + 音量滑条(上限 vol_max、平方锥度非线性标注)。
|
||||
* 写级——前端切换/调高前 confirmDlg(选错口会无声)。当前值只读显示锥度后百分比(上限以内)。 */
|
||||
function audioCtlHtml(d) {
|
||||
const vmax = (d && d.vol_max != null) ? d.vol_max : 100;
|
||||
const vol = (d && d.vol != null && d.vol >= 0) ? Math.min(d.vol, vmax) : 0;
|
||||
const cur = d.port;
|
||||
let s = '<div class="card"><h3 class="card-title">路由与音量</h3><table class="table"><tbody>' +
|
||||
row('路由模式', esc(d.route_mode || '—') + (d.route_mode === 'manual' ? '(手动覆盖)' : '(跟随插孔)')) +
|
||||
row('输出口', audioPortName(d.port)) +
|
||||
row('音量(只读)', (d.vol != null && d.vol >= 0 ? '<span class="' + (d.vol > vmax ? 'urgency-hi' : '') + '">' + esc(d.vol) + ' %</span>' : '—') +
|
||||
' <span class="muted">(上限 ' + esc(vmax) + ' · 平方锥度非线性)</span>') +
|
||||
'</tbody></table>';
|
||||
s += '<div class="field-row" style="flex-wrap:wrap;gap:8px;margin-top:6px"><span class="muted" style="width:auto">输出路由</span>';
|
||||
[0, 2, 3].forEach(p => {
|
||||
s += '<button class="btn ' + (p === cur ? 'btn-primary' : 'btn-ghost') + '" data-audio-route="' + p + '">' + audioRouteLabel(p) + '</button>';
|
||||
});
|
||||
s += '</div>';
|
||||
s += '<div class="field-row" style="margin-top:8px"><span class="muted" style="width:44px">' + vol + '</span>' +
|
||||
'<input type="range" min="0" max="' + vmax + '" value="' + vol + '" data-audio-vol>' +
|
||||
'<span class="muted" style="width:44px;text-align:right">' + vmax + '</span></div>';
|
||||
s += '<p class="muted">切换/调节前会确认。选错输出口会<b>无声</b>;音量走<b>平方锥度</b>(越接近上限越响,非等比),系统上限 ' + esc(vmax) + '。</p>';
|
||||
s += '</div>';
|
||||
return s;
|
||||
}
|
||||
function audioFlowHtml(d) {
|
||||
let h = '';
|
||||
if (!d || !d.available) {
|
||||
@@ -532,11 +562,7 @@
|
||||
h += '<p class="muted">当前无活动音频流。</p>';
|
||||
}
|
||||
h += '</div>';
|
||||
h += '<div class="card"><h3 class="card-title">路由与音量</h3><table class="table"><tbody>' +
|
||||
row('路由模式', esc(d.route_mode || '—') + (d.route_mode === 'manual' ? '(手动覆盖)' : '(跟随插孔)')) +
|
||||
row('输出口', audioPortName(d.port)) +
|
||||
row('音量', (d.vol != null && d.vol >= 0 ? esc(d.vol) + ' %' : '—')) +
|
||||
'</tbody></table></div>';
|
||||
h += audioCtlHtml(d);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
@@ -550,13 +576,14 @@
|
||||
return;
|
||||
}
|
||||
if (state.route !== 'audio') return;
|
||||
if (audioVolDrag) return; // 音量滑条拖拽中:暂停重渲染防打断(电平/控件保留当前交互态)
|
||||
flowEl.innerHTML = audioFlowHtml(d);
|
||||
updateLevelMeter($('#audio-level'), d);
|
||||
const note = $('#audio-level-note');
|
||||
if (note) {
|
||||
if (!d || !d.available) note.innerHTML = 'audiod 不可用 · 每 1 秒自动刷新 · 只读展示。';
|
||||
else if (!d.active) note.innerHTML = '当前无流 · 电平保持 −∞ · 每 1 秒自动刷新。';
|
||||
else note.innerHTML = '每 1 秒自动刷新 · 只读展示(测试音请用设备 CLI <code>meow_audio tone</code>,面板不放声)。';
|
||||
if (!d || !d.available) note.innerHTML = 'audiod 不可用 · 每 1 秒自动刷新(路由/音量写端点不可用)。';
|
||||
else if (!d.active) note.innerHTML = '当前无流 · 电平保持 −∞ · 每 1 秒自动刷新。路由/音量为写级,需登录会话。';
|
||||
else note.innerHTML = '每 1 秒自动刷新 · 流信息只读;路由与音量为写级(确认后生效,不放测试音)。';
|
||||
}
|
||||
}
|
||||
function renderAudio() {
|
||||
@@ -573,12 +600,56 @@
|
||||
fetchAudioOnce();
|
||||
audioTimer = setInterval(fetchAudioOnce, 1000);
|
||||
}
|
||||
/* 音频页交互(委托 #content,元素随 1s 轮询重建亦不失效):路由切换/音量滑条均写级 + confirm。 */
|
||||
function bindAudio() {
|
||||
$('#content').addEventListener('click', async function (e) {
|
||||
const rb = e.target.closest('[data-audio-route]');
|
||||
if (rb) {
|
||||
const port = parseInt(rb.dataset.audioRoute, 10);
|
||||
if (!(port === 0 || port === 2 || port === 3)) { toast('非法路由口(仅自动/3.5/4.4)'); return; }
|
||||
const ok = await confirmDlg('切换输出路由',
|
||||
'确认切换到「' + audioRouteLabel(port) + (port === 0 ? '(跟随插孔)' : '') + '」?\n选错输出口会无声,请确认当前插孔。', false);
|
||||
if (!ok) return;
|
||||
try { await api('POST', '/api/audio/route', { port: port }); toast('路由已切换:' + audioRouteLabel(port)); }
|
||||
catch (err) { toast('切换失败:' + errText(err)); }
|
||||
return;
|
||||
}
|
||||
});
|
||||
$('#content').addEventListener('pointerdown', function (e) {
|
||||
const sl = e.target.closest('[data-audio-vol]');
|
||||
if (sl) { audioVolDrag = true; const row = sl.closest('.field-row'); if (row) row.querySelector('.muted').textContent = sl.value; }
|
||||
});
|
||||
$('#content').addEventListener('pointerup', function (e) {
|
||||
// 释放即复位;若值已提交 change 会再走确认,此处仅保证「点击未改动」时也不卡住重渲染
|
||||
if (e.target.closest('[data-audio-vol]')) setTimeout(function () { audioVolDrag = false; }, 250);
|
||||
});
|
||||
$('#content').addEventListener('input', function (e) {
|
||||
const sl = e.target.closest('[data-audio-vol]');
|
||||
if (!sl) return;
|
||||
const row = sl.closest('.field-row'); if (row) row.querySelector('.muted').textContent = sl.value;
|
||||
});
|
||||
$('#content').addEventListener('change', async function (e) {
|
||||
const sl = e.target.closest('[data-audio-vol]');
|
||||
if (!sl) return;
|
||||
audioVolDrag = false;
|
||||
const v = parseInt(sl.value, 10);
|
||||
const vmax = parseInt(sl.max, 10) || 100;
|
||||
const ok = await confirmDlg('确认音量',
|
||||
'确认将音量设为 ' + v + ' %?(系统上限 ' + vmax + ',平方锥度非线性——越接近上限越响且非等比)', false);
|
||||
if (!ok) { fetchAudioOnce(); return; } // 取消 → 立即拉一次复位滑条
|
||||
try {
|
||||
await api('POST', '/api/audio/volume', { value: v });
|
||||
toast('音量已设为 ' + v + ' %');
|
||||
fetchAudioOnce();
|
||||
} catch (err) { toast('设置失败:' + errText(err)); fetchAudioOnce(); }
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 2d. 首页仪表盘(#/home,默认路由;/api/overview 聚合,5s 节拍 + 电平 1s 子节拍) ----------------
|
||||
* 单请求 /api/overview 供全部卡片;另低频补充 /api/system(boot/默认启动,静态)+ /api/net/dns(DNS 上游/缓存)。
|
||||
* 任一源 available:false / 缺失 → 该卡显示「—」,绝不拖垮其它卡。 */
|
||||
let homeTimer = null, homeLevelTimer = null;
|
||||
let homeData = null, homeDns = null, homeSys = null;
|
||||
let homeData = null, homeSys = null;
|
||||
function stopHome() {
|
||||
if (homeTimer) { clearInterval(homeTimer); homeTimer = null; }
|
||||
if (homeLevelTimer) { clearInterval(homeLevelTimer); homeLevelTimer = null; }
|
||||
@@ -643,7 +714,7 @@
|
||||
const nd = homeData && homeData.net;
|
||||
if (!isAvail(nd)) return homeCard('WiFi / DNS', homeUnavailable());
|
||||
const w = nd.wifi || {};
|
||||
const dn = homeDns;
|
||||
const dn = (nd && nd.dns) || null; // 批2: DNS 统计已并入 /api/overview 的 net 块(首页严格单请求)
|
||||
let rows = '';
|
||||
rows += homeKv('WiFi', w.connected ? ('<span class="home-chip ok">已连接</span> ' + esc(w.ssid || '')) : '<span class="home-chip warn">未连接</span>');
|
||||
if (w.connected) {
|
||||
@@ -733,24 +804,23 @@
|
||||
}
|
||||
async function fetchOverviewOnce() {
|
||||
if (state.route !== 'home') return;
|
||||
const r = await Promise.allSettled([api('GET', '/api/net/dns'), api('GET', '/api/overview')]);
|
||||
if (r[1].status !== 'fulfilled') {
|
||||
const r = await Promise.allSettled([api('GET', '/api/overview')]);
|
||||
if (r[0].status !== 'fulfilled') {
|
||||
const b = $('#home-banner');
|
||||
if (b) {
|
||||
b.innerHTML = '<span class="ico">⚠️</span><span>总览加载失败:' + esc(errText(r[1].reason)) + '</span><button class="btn" id="home-retry">重试</button>';
|
||||
b.innerHTML = '<span class="ico">⚠️</span><span>总览加载失败:' + esc(errText(r[0].reason)) + '</span><button class="btn" id="home-retry">重试</button>';
|
||||
b.hidden = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const b = $('#home-banner'); if (b) { b.hidden = true; b.innerHTML = ''; }
|
||||
homeDns = (r[0].status === 'fulfilled') ? r[0].value : null;
|
||||
homeData = r[1].value;
|
||||
homeData = r[0].value; // 首页严格单请求:DNS 已并入 overview 的 net 块
|
||||
renderHomeCards();
|
||||
}
|
||||
function renderHome() {
|
||||
stopHome();
|
||||
resetSpark(); // 进入首页重置 sparkline 累积
|
||||
homeData = null; homeDns = null; homeSys = null;
|
||||
homeData = null; homeSys = null;
|
||||
$('#content').innerHTML =
|
||||
'<div class="page" id="home-page">' +
|
||||
'<div id="home-banner" class="page-banner" hidden></div>' +
|
||||
@@ -1863,27 +1933,86 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 8. 日志页 ---------------- */
|
||||
async function renderLogs() {
|
||||
$('#content').innerHTML = '<div class="page" id="logs-page"><p class="placeholder">加载日志列表…</p></div>';
|
||||
let d;
|
||||
try { d = await api('GET', '/api/logs'); }
|
||||
catch (e) { if (state.route === 'logs') $('#content').innerHTML = emptyState('加载失败:' + errText(e), e.code); return; }
|
||||
if (state.route !== 'logs') return;
|
||||
let h = '<div class="page" id="logs-page">';
|
||||
h += '<div class="card"><h3 class="card-title">日志文件(MeowOS/logs)</h3>';
|
||||
h += '<div class="field-row" style="margin-bottom:12px"><span class="muted">' + esc(d.dir || '') + '</span></div>';
|
||||
h += '<table class="table"><thead><tr><th>文件</th><th>大小</th><th>修改时间</th><th style="text-align:right">操作</th></tr></thead><tbody>';
|
||||
if (!d.files.length) h += '<tr><td colspan="4" class="empty">暂无日志</td></tr>';
|
||||
d.files.forEach(f => {
|
||||
h += '<tr><td class="mono">' + esc(f.name) + '</td><td class="muted">' + fmtBytes(f.size) + '</td><td class="muted">' + fmtTime(f.mtime) + '</td>' +
|
||||
/* ---------------- 8. 日志页(批2 崩溃/日志中心:崩溃汇总 + 分组 + 前缀过滤 + 崩溃尾巴前置) ---------------- */
|
||||
let logsGroup = 'all', logsPrefix = '';
|
||||
const LOG_GROUP_NAME = { all: '全部', system: '系统服务', app: 'App', audio_monitor: 'audio_monitor', power: 'power', net: 'net', crash: 'crash', ams: 'ams' };
|
||||
function groupName(g) { return LOG_GROUP_NAME[g] || (g || 'app'); }
|
||||
function logsCrashCard(cr) {
|
||||
const ents = (cr.entities || []).filter(e => e.name);
|
||||
let h = '<div class="card"><h3 class="card-title">崩溃汇总(' + esc(cr.file || '本 boot · crash log') + ')</h3>';
|
||||
if (!ents.length && !(cr.dmesg_count || 0)) { h += '<p class="muted">本 boot 无崩溃记录。</p></div>'; return h; }
|
||||
h += '<table class="table"><thead><tr><th>实体</th><th>崩溃数</th><th>最近时间</th><th>信号/退出</th><th style="text-align:right">操作</th></tr></thead><tbody>';
|
||||
ents.forEach(e => {
|
||||
h += '<tr><td class="mono">' + esc(e.name) + '</td>' +
|
||||
'<td class="' + ((e.count || 0) > 0 ? 'crash-hi' : 'muted') + '">' + esc(e.count) + ((e.count || 0) > 0 ? ' ⚠' : '') + '</td>' +
|
||||
'<td class="muted">' + esc(e.last_time || '—') + '</td>' +
|
||||
'<td><span class="muted">' + esc(e.last_kind || '—') + '</span>' + (e.last_code != null && e.last_code !== 0 ? ' (' + esc(e.last_code) + ')' : '') + '</td>' +
|
||||
'<td><div class="row-actions"><button class="btn btn-ghost" data-crash-tail="' + escSafeAttr(e.name) + '">stdout 尾部</button></div></td></tr>';
|
||||
});
|
||||
h += '</tbody></table>';
|
||||
if (cr.dmesg_count) {
|
||||
h += '<p class="muted">内核 oops / segfault 捕获:<b>' + esc(cr.dmesg_count) + '</b> 条' +
|
||||
((cr.dmesg_tail || []).length ? '<pre class="log-view">' + esc((cr.dmesg_tail || []).map(x => '· ' + x).join('\n')) + '</pre>' : '') + '</p>';
|
||||
}
|
||||
h += '</div>';
|
||||
return h;
|
||||
}
|
||||
function renderLogsFiles(files) {
|
||||
const el = $('#logs-files');
|
||||
if (!el) return;
|
||||
const filtered = files.filter(f => {
|
||||
if (logsGroup !== 'all' && (f.group || 'app') !== logsGroup) return false;
|
||||
if (logsPrefix && !f.name.toLowerCase().startsWith(logsPrefix.toLowerCase())) return false;
|
||||
return true;
|
||||
});
|
||||
let h = '<table class="table"><thead><tr><th>文件</th><th>分组</th><th>大小</th><th>修改时间</th><th style="text-align:right">操作</th></tr></thead><tbody>';
|
||||
if (!filtered.length) h += '<tr><td colspan="5" class="empty">暂无日志(或过滤后无结果)</td></tr>';
|
||||
filtered.forEach(f => {
|
||||
h += '<tr><td class="mono">' + esc(f.name) + '</td><td class="muted">' + groupName(f.group) + '</td><td class="muted">' + fmtBytes(f.size) + '</td><td class="muted">' + fmtTime(f.mtime) + '</td>' +
|
||||
'<td><div class="row-actions"><button class="btn btn-ghost" data-log="' + esc(f.name) + '">查看</button></div></td></tr>';
|
||||
});
|
||||
h += '</tbody></table></div><pre class="log-view" id="log-view" hidden></pre></div>';
|
||||
h += '</tbody></table>';
|
||||
el.innerHTML = h;
|
||||
}
|
||||
async function renderLogs() {
|
||||
$('#content').innerHTML = '<div class="page" id="logs-page"><p class="placeholder">加载日志列表…</p></div>';
|
||||
const r = await Promise.allSettled([api('GET', '/api/logs'), api('GET', '/api/logs/crash')]);
|
||||
if (state.route !== 'logs') return;
|
||||
if (r[0].status !== 'fulfilled') {
|
||||
$('#content').innerHTML = emptyState('加载失败:' + errText(r[0].reason), r[0].reason && r[0].reason.code);
|
||||
return;
|
||||
}
|
||||
const d = r[0].value;
|
||||
const cr = (r[1].status === 'fulfilled') ? r[1].value : null;
|
||||
let h = '<div class="page" id="logs-page">';
|
||||
if (cr && ((cr.count || 0) > 0 || (cr.dmesg_count || 0) > 0 || (cr.entities || []).length)) h += logsCrashCard(cr);
|
||||
h += '<div class="card"><h3 class="card-title">日志文件(MeowOS/logs)</h3>';
|
||||
h += '<div class="field-row" style="margin-bottom:8px"><span class="muted">' + esc(d.dir || '') + '</span></div>';
|
||||
h += '<div class="field-row" style="flex-wrap:wrap;gap:6px;margin-bottom:12px">';
|
||||
h += '<input class="log-prefix" data-log-prefix placeholder="按文件名前缀过滤" value="' + escSafeAttr(logsPrefix) + '" style="flex:1;min-width:140px">';
|
||||
h += '</div>';
|
||||
h += '<div class="field-row" style="flex-wrap:wrap;gap:6px;margin-bottom:12px">';
|
||||
Object.keys(LOG_GROUP_NAME).forEach(g => {
|
||||
h += '<button class="btn ' + (logsGroup === g ? 'btn-primary' : 'btn-ghost') + '" data-log-group="' + g + '">' + LOG_GROUP_NAME[g] + '</button>';
|
||||
});
|
||||
h += '</div>';
|
||||
h += '<div id="logs-files"><table class="table"><tbody><tr><td class="empty">加载…</td></tr></tbody></table></div>';
|
||||
h += '</div><pre class="log-view" id="log-view" hidden></pre></div>';
|
||||
$('#content').innerHTML = h;
|
||||
logsFiles = d.files || [];
|
||||
renderLogsFiles(logsFiles);
|
||||
}
|
||||
function bindLogs() {
|
||||
$('#content').addEventListener('click', async function (e) {
|
||||
const gbtn = e.target.closest('[data-log-group]');
|
||||
if (gbtn) {
|
||||
logsGroup = gbtn.dataset.logGroup;
|
||||
$$('[data-log-group]').forEach(b => b.classList.toggle('btn-primary', b === gbtn));
|
||||
renderLogsFiles(logsFiles);
|
||||
return;
|
||||
}
|
||||
const tb = e.target.closest('[data-crash-tail]');
|
||||
if (tb) { await viewLogsCrashTail(tb.dataset.crashTail, tb); return; }
|
||||
const t = e.target.closest('[data-log]'); if (!t) return;
|
||||
const p = $('#log-view');
|
||||
try {
|
||||
@@ -1891,6 +2020,32 @@
|
||||
p.hidden = false; p.textContent = (d.lines || []).join('\n') + (d.truncated ? '\n…(更多行被截断)' : '');
|
||||
} catch (err) { toast('查看失败:' + errText(err)); }
|
||||
});
|
||||
$('#content').addEventListener('input', function (e) {
|
||||
const p = e.target.closest('[data-log-prefix]');
|
||||
if (!p) return;
|
||||
logsPrefix = p.value;
|
||||
renderLogsFiles(logsFiles);
|
||||
});
|
||||
}
|
||||
/* 崩溃实体 → stdout 尾部:复用批1-B 的读取路径(/api/files 列 logs/<pkg>/ → /api/logs/read 读尾部)。 */
|
||||
let logsFiles = [];
|
||||
async function viewLogsCrashTail(name, btn) {
|
||||
btn.textContent = '加载中…';
|
||||
let file;
|
||||
try {
|
||||
const d = await api('GET', '/api/files?path=' + encodeURIComponent('/MeowOS/logs/' + name + '/'));
|
||||
const files = (d.items || []).filter(f => f.type === 'file' && f.name && f.name.indexOf('stdout_') === 0);
|
||||
if (!files.length) { btn.textContent = 'stdout 尾部'; toast('未找到 ' + name + ' 的 stdout 日志'); return; }
|
||||
files.sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
|
||||
file = files[0].name;
|
||||
} catch (err) { btn.textContent = 'stdout 尾部'; toast('查找失败:' + errText(err)); return; }
|
||||
try {
|
||||
const d = await api('GET', '/api/logs/read?file=' + encodeURIComponent(name + '/' + file) + '&lines=200');
|
||||
const lines = (d.lines || []);
|
||||
const p = $('#log-view');
|
||||
if (p) { p.hidden = false; p.textContent = name + '/' + file + '\n' + (lines.join('\n') || '(空)') + (d.truncated ? '\n…(超出尾部上限,部分被截断)' : ''); }
|
||||
} catch (err) { toast('读取失败:' + errText(err)); }
|
||||
btn.textContent = 'stdout 尾部';
|
||||
}
|
||||
|
||||
/* ---------------- 9. 通知页 ---------------- */
|
||||
@@ -2132,7 +2287,7 @@
|
||||
home: { title: '首页仪表盘', render: renderHome, bind: bindHome },
|
||||
perf: { title: '实时性能', render: renderPerf, bind: null },
|
||||
bright: { title: '亮度 · 音量', render: renderBright, bind: null },
|
||||
audio: { title: '音频状态', render: renderAudio, bind: null },
|
||||
audio: { title: '音频状态', render: renderAudio, bind: bindAudio },
|
||||
net: { title: '网络管理', render: renderNet, bind: bindNet },
|
||||
files: { title: '文件管理', render: renderFiles, bind: bindFiles },
|
||||
apps: { title: 'MeowAPPs 管理', render: renderApps, bind: bindApps },
|
||||
@@ -2181,7 +2336,7 @@
|
||||
bindNav();
|
||||
bindThemeUi();
|
||||
bindSlider();
|
||||
bindFiles(); bindApps(); bindFonts(); bindSettings(); bindRemote(); bindScreen(); bindSystem(); bindLogs(); bindNotif(); bindNet(); bindHome(); bindProc(); bindPin();
|
||||
bindFiles(); bindApps(); bindFonts(); bindSettings(); bindRemote(); bindScreen(); bindSystem(); bindLogs(); bindNotif(); bindNet(); bindHome(); bindProc(); bindPin(); bindAudio();
|
||||
bindLogin();
|
||||
bindPwDlg();
|
||||
// 首次进入:访问级 PIN 鉴权——已登录直接进;未设 PIN(且无旧密码)→ 首次设置流程;已设 → 登录
|
||||
|
||||
Reference in new issue
Block a user