优化Web管理页状态和参数说明
This commit is contained in:
parent
a6ee43d6af
commit
5f88b2198e
@ -324,8 +324,13 @@ const char* index_html()
|
|||||||
.warn { color:#9a3412; }
|
.warn { color:#9a3412; }
|
||||||
.ok { color:#166534; }
|
.ok { color:#166534; }
|
||||||
.muted { color:#667785; }
|
.muted { color:#667785; }
|
||||||
|
.help { margin-top:4px; font-size:12px; line-height:1.35; color:#667785; }
|
||||||
|
.status-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px; margin-bottom:12px; }
|
||||||
|
.status-box { border:1px solid #e1e7ed; border-radius:6px; padding:10px; background:#fbfcfd; }
|
||||||
|
.status-box strong { display:block; margin-bottom:4px; color:#243746; }
|
||||||
|
.url-cell { max-width:360px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
#toast { min-height:24px; margin-left:6px; }
|
#toast { min-height:24px; margin-left:6px; }
|
||||||
@media (max-width: 860px) { .grid, .grid.two { grid-template-columns:1fr; } table { display:block; overflow-x:auto; } }
|
@media (max-width: 860px) { .grid, .grid.two, .status-grid { grid-template-columns:1fr; } table { display:block; overflow-x:auto; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -419,6 +424,35 @@ async function api(path, options) {
|
|||||||
|
|
||||||
function setToast(text, cls='muted') { const t=$('toast'); t.className=cls; t.textContent=text; }
|
function setToast(text, cls='muted') { const t=$('toast'); t.className=cls; t.textContent=text; }
|
||||||
|
|
||||||
|
function helpFor(id, text) {
|
||||||
|
const el = $(id);
|
||||||
|
if (!el || !el.parentElement || el.parentElement.querySelector('.help')) return;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'help';
|
||||||
|
div.textContent = text;
|
||||||
|
el.parentElement.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
function installHelp() {
|
||||||
|
helpFor('srs_root', 'SRS安装根目录,安装脚本会把 srs/bin、conf、html 放在这里。');
|
||||||
|
helpFor('record_config', 'video服务读取这个SRS录像配置来解析录像目录、HTTP端口和切片时长。');
|
||||||
|
helpFor('public_interface', '用于生成对外播放地址的网卡名,比如 enP2p33s0;取不到IP会回退到127.0.0.1。');
|
||||||
|
helpFor('stream_app', 'RTMP路径里的app名,默认 camera,对应 rtmp://host/app/stream。');
|
||||||
|
helpFor('live_enabled', '控制是否向 live SRS 推实时流,关闭后不会生成实时播放分支。');
|
||||||
|
helpFor('record_enabled', '控制是否向 record SRS 推流并由SRS写录像。');
|
||||||
|
helpFor('record_path', 'SRS DVR保存mp4切片的根目录,目标设备上要确保可写。');
|
||||||
|
helpFor('dvr_duration', '每个录像文件的切片长度,单位秒。');
|
||||||
|
helpFor('retention_days', '录像保留天数,超过后由video服务扫描清理。');
|
||||||
|
helpFor('usage_threshold', '磁盘使用率阈值,0.9表示90%,超过后按最老小时清理。');
|
||||||
|
helpFor('live_rtmp_port', 'video服务把实时流推到这个RTMP端口。');
|
||||||
|
helpFor('live_http_api_port', 'SRS WHEP/WebRTC播放接口端口,页面和MQTT返回的实时播放URL使用它。');
|
||||||
|
helpFor('live_http_server_port', 'SRS自带静态页面和HTTP-FLV调试入口。');
|
||||||
|
helpFor('live_rtc_port', 'WebRTC UDP媒体端口,需要网络放通。');
|
||||||
|
helpFor('record_rtmp_port', 'video服务把录像流推到这个RTMP端口。');
|
||||||
|
helpFor('record_http_api_port', 'record实例的SRS HTTP API端口。');
|
||||||
|
helpFor('record_http_server_port', '录像文件HTTP访问端口,回放URL会走这个HTTP server。');
|
||||||
|
}
|
||||||
|
|
||||||
function fillConfig() {
|
function fillConfig() {
|
||||||
const m = config.mqtt_server || {};
|
const m = config.mqtt_server || {};
|
||||||
const s = config.srs || {};
|
const s = config.srs || {};
|
||||||
@ -497,12 +531,42 @@ async function loadConfig() { config = await api('/api/config'); fillConfig(); }
|
|||||||
async function loadStatus() {
|
async function loadStatus() {
|
||||||
const st = await api('/api/status');
|
const st = await api('/api/status');
|
||||||
const disk = st.disk || {};
|
const disk = st.disk || {};
|
||||||
|
$('status').innerHTML = renderStatus(st, disk);
|
||||||
|
return;
|
||||||
$('status').innerHTML = `
|
$('status').innerHTML = `
|
||||||
<div>对外IP:<span class="pill">${st.public_ip || 'unknown'}</span></div>
|
<div>对外IP:<span class="pill">${st.public_ip || 'unknown'}</span></div>
|
||||||
<div>录像磁盘:${disk.ok ? `<span class="ok">${disk.used_percent.toFixed(1)}% used</span>` : `<span class="warn">${disk.error || 'unavailable'}</span>`}</div>
|
<div>录像磁盘:${disk.ok ? `<span class="ok">${disk.used_percent.toFixed(1)}% used</span>` : `<span class="warn">${disk.error || 'unavailable'}</span>`}</div>
|
||||||
<div>服务:${(st.services||[]).map(s => `${s.name}: <span class="${s.active==='active'?'ok':'warn'}">${s.active}</span>`).join(' | ')}</div>
|
<div>服务:${(st.services||[]).map(s => `${s.name}: <span class="${s.active==='active'?'ok':'warn'}">${s.active}</span>`).join(' | ')}</div>
|
||||||
<div>通道:${(st.channels||[]).map(c => `${c.name} ${c.device_exists?'':'(无设备)'} ${c.running?'运行':'停止'}`).join(',')}</div>`;
|
<div>通道:${(st.channels||[]).map(c => `${c.name} ${c.device_exists?'':'(无设备)'} ${c.running?'运行':'停止'}`).join(',')}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(v) {
|
||||||
|
return String(v ?? '').replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatus(st, disk) {
|
||||||
|
const services = (st.services || []).map(s => `${escapeHtml(s.name)}: <span class="${s.active==='active'?'ok':'warn'}">${escapeHtml(s.active)}</span>`).join('<br>');
|
||||||
|
const diskText = disk.ok ? `<span class="ok">${Number(disk.used_percent || 0).toFixed(1)}% used</span><div class="muted">${escapeHtml(disk.path)}</div>` : `<span class="warn">${escapeHtml(disk.error || 'unavailable')}</span>`;
|
||||||
|
const channelRows = (st.channels || []).map(c => {
|
||||||
|
const running = c.running ? '<span class="ok">运行</span>' : '<span class="warn">停止</span>';
|
||||||
|
const enabled = c.enabled ? '<span class="ok">启用</span>' : '<span class="muted">禁用</span>';
|
||||||
|
const exists = c.device_exists ? '<span class="ok">存在</span>' : '<span class="warn">不存在</span>';
|
||||||
|
const reason = c.reason || (c.enabled ? '' : '通道已禁用');
|
||||||
|
const url = c.url ? `<span title="${escapeHtml(c.url)}">${escapeHtml(c.url)}</span>` : '<span class="muted">无</span>';
|
||||||
|
return `<tr><td>${escapeHtml(c.name)}</td><td>${escapeHtml(c.device)}</td><td>${enabled}</td><td>${exists}</td><td>${running}</td><td>${escapeHtml(reason)}</td><td class="url-cell">${url}</td></tr>`;
|
||||||
|
}).join('');
|
||||||
|
return `
|
||||||
|
<div class="status-grid">
|
||||||
|
<div class="status-box"><strong>对外IP</strong><span class="pill">${escapeHtml(st.public_ip || 'unknown')}</span></div>
|
||||||
|
<div class="status-box"><strong>录像磁盘</strong>${diskText}</div>
|
||||||
|
<div class="status-box"><strong>服务</strong>${services || '<span class="muted">无</span>'}</div>
|
||||||
|
</div>
|
||||||
|
<table class="status-table">
|
||||||
|
<thead><tr><th>通道</th><th>设备</th><th>配置</th><th>设备节点</th><th>推流状态</th><th>原因</th><th>播放地址</th></tr></thead>
|
||||||
|
<tbody>${channelRows || '<tr><td colspan="7" class="muted">无通道</td></tr>'}</tbody>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
try {
|
try {
|
||||||
collectConfig();
|
collectConfig();
|
||||||
@ -510,7 +574,7 @@ async function saveConfig() {
|
|||||||
setToast(res.message || '已保存,重启服务后生效', res.ok ? 'ok' : 'warn');
|
setToast(res.message || '已保存,重启服务后生效', res.ok ? 'ok' : 'warn');
|
||||||
} catch (e) { setToast(e.message, 'warn'); }
|
} catch (e) { setToast(e.message, 'warn'); }
|
||||||
}
|
}
|
||||||
async function loadAll() { try { await loadConfig(); await loadStatus(); setToast('已刷新'); } catch(e) { setToast(e.message, 'warn'); } }
|
async function loadAll() { try { await loadConfig(); installHelp(); await loadStatus(); setToast('已刷新'); } catch(e) { setToast(e.message, 'warn'); } }
|
||||||
loadAll();
|
loadAll();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user