video_monitor/templates/app/system/config.html
2026-08-30 22:23:12 +08:00

852 lines
43 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "app/base.html" %}
{% load static %}
{% block title %}{{ T.page_config|default:'启动配置' }} - {{ settings.name|default:"Monitor" }}{% endblock %}
{% block nav_config %}active{% endblock %}
{% block page_title %}{{ T.page_config|default:'启动配置' }}{% endblock %}
{% block extra_head %}
<style>
.cfg-wrap { max-width: 1080px; margin: 0 auto; }
/* ============ 顶部 Tab 切换 ============ */
.cfg-tabs {
display: flex;
background: #fff;
border-radius: 8px;
border: 1px solid var(--c-border-light, #eef2f5);
margin-bottom: 16px;
overflow-x: auto;
}
.cfg-tab {
flex: 1;
min-width: 110px;
padding: 12px 16px;
text-align: center;
font-size: 13px;
color: #6b7280;
background: transparent;
border: none;
border-right: 1px solid var(--c-border-light, #eef2f5);
cursor: pointer;
transition: all .15s;
white-space: nowrap;
font-weight: 500;
position: relative;
}
.cfg-tab:last-child { border-right: none; }
.cfg-tab:hover { background: #fafbfc; color: #374151; }
.cfg-tab.active {
color: #169F85;
font-weight: 600;
background: #f0f9f6;
}
.cfg-tab.active::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: 2px;
background: #169F85;
}
/* ============ 卡片 ============ */
.cfg-card {
background: #fff;
border-radius: 8px;
border: 1px solid var(--c-border-light, #eef2f5);
margin-bottom: 14px;
display: none;
}
.cfg-card.active { display: block; }
.cfg-card-head {
display: flex;
align-items: center;
padding: 14px 20px;
border-bottom: 1px solid var(--c-border-light, #eef2f5);
}
.cfg-card-head h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #1f2937;
display: flex;
align-items: center;
gap: 8px;
}
.cfg-card-head h3::before {
content: "";
display: inline-block;
width: 3px;
height: 14px;
background: #169F85;
border-radius: 2px;
}
.cfg-card-head .head-tip {
margin-left: auto;
font-size: 12px;
color: #9ca3af;
}
.cfg-card-body { padding: 16px 20px; }
/* ============ 两列布局 ============ */
.cfg-cols {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 32px;
}
@media (max-width: 820px) { .cfg-cols { grid-template-columns: 1fr; } }
/* ============ 表单组 ============ */
.cfg-form-group {
display: flex;
align-items: center;
padding: 8px 0;
min-height: 38px;
border-bottom: 1px solid #f3f4f6;
}
.cfg-form-group:last-child { border-bottom: none; }
.cfg-form-group .cfg-label {
flex: 0 0 36%;
font-size: 12px;
color: #6b7280;
font-weight: 500;
padding-right: 12px;
}
.cfg-form-group .cfg-control {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
/* ============ 输入控件 ============ */
.cfg-input, .cfg-select {
width: 100%;
padding: 6px 10px;
font-size: 13px;
border: 1px solid #d1d5db;
border-radius: 4px;
background: #fff;
color: #1f2937;
transition: border-color .15s, box-shadow .15s;
height: 32px;
box-sizing: border-box;
}
.cfg-input:hover, .cfg-select:hover { border-color: #9ca3af; }
.cfg-input:focus, .cfg-select:focus {
outline: none;
border-color: #169F85;
box-shadow: 0 0 0 2px rgba(22,159,133,0.12);
}
.cfg-input.small { width: 100px; flex-shrink: 0; }
.cfg-input.middle { width: 64px; flex-shrink: 0; text-align: center; }
.cfg-hint { font-size: 11px; color: #9ca3af; margin-left: 4px; }
/* ============ 单选按钮组(按钮式) ============ */
.cfg-radio-group {
display: inline-flex;
border-radius: 4px;
overflow: hidden;
border: 1px solid #d1d5db;
}
.cfg-radio-group input { display: none; }
.cfg-radio-opt {
padding: 5px 14px;
font-size: 12px;
color: #6b7280;
background: #fff;
cursor: pointer;
transition: all .15s;
user-select: none;
border-right: 1px solid #d1d5db;
}
.cfg-radio-opt:last-child { border-right: none; }
.cfg-radio-opt:hover { background: #f3f4f6; color: #374151; }
.cfg-radio-group input:checked + .cfg-radio-opt {
background: #169F85;
color: #fff;
font-weight: 500;
}
/* ============ 开关 ============ */
.cfg-switch-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.cfg-switch-row .cfg-switch-label { font-size: 12px; color: #9ca3af; }
/* ============ 底部操作栏 ============ */
.cfg-actions {
background: #fff;
border: 1px solid var(--c-border-light, #eef2f5);
border-radius: 8px;
padding: 12px 20px;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
position: sticky;
bottom: 0;
z-index: 10;
}
.cfg-save-tip {
margin-right: auto;
font-size: 12px;
color: #9ca3af;
}
.cfg-save-tip.danger { color: #b91c1c; }
.cfg-btn {
padding: 7px 18px;
font-size: 13px;
font-weight: 500;
border-radius: 4px;
border: 1px solid #d1d5db;
background: #fff;
color: #4b5563;
cursor: pointer;
transition: all .15s;
display: inline-flex;
align-items: center;
gap: 4px;
}
.cfg-btn:hover { color: #169F85; border-color: #169F85; background: #f0f9f6; }
.cfg-btn-primary {
background: #169F85;
color: #fff;
border-color: #169F85;
}
.cfg-btn-primary:hover {
background: #14b89a;
border-color: #14b89a;
color: #fff;
}
.cfg-btn-primary:disabled { opacity: .6; cursor: not-allowed; background: #169F85; }
/* 加载转圈 */
.cfg-spinner {
display: inline-block;
width: 12px; height: 12px;
border: 2px solid #e5e7eb;
border-top: 2px solid #169F85;
border-radius: 50%;
animation: cfg-spin 1s linear infinite;
vertical-align: middle;
}
@keyframes cfg-spin { 0% { transform: rotate(0); } 100% { transform: rotate(360deg); } }
</style>
{% endblock %}
{% block content %}
<div class="cfg-wrap">
<!-- 页面头部 -->
<div class="page-head">
<div class="page-head-left">
<div>
<div class="page-head-title">{{ T.page_config|default:'启动配置' }}</div>
<div class="page-head-sub">{{ T.config_subtitle|default:'基础参数、GB28181 信令与品牌信息统一管理' }}</div>
</div>
</div>
</div>
<!-- Tab 切换条 -->
<div class="cfg-tabs">
<button class="cfg-tab active" data-tab="tab_basic" onclick="switchCfgTab('tab_basic')">{{ T.tab_basic|default:'基础配置' }}</button>
<button class="cfg-tab" data-tab="tab_media" onclick="switchCfgTab('tab_media')">{{ T.tab_media|default:'流媒体' }}</button>
<button class="cfg-tab" data-tab="tab_analysis" onclick="switchCfgTab('tab_analysis')">{{ T.tab_analysis|default:'分析' }}</button>
<button class="cfg-tab" data-tab="tab_record" onclick="switchCfgTab('tab_record')">{{ T.tab_record|default:'录像' }}</button>
<button class="cfg-tab" data-tab="tab_gb28181" onclick="switchCfgTab('tab_gb28181')">{{ T.tab_gb28181|default:'GB28181' }}</button>
<button class="cfg-tab" data-tab="tab_oem" onclick="switchCfgTab('tab_oem')">{{ T.tab_branding|default:'品牌信息' }}</button>
</div>
<!-- 卡片1基础配置 -->
<div class="cfg-card active" data-card="tab_basic">
<div class="cfg-card-head"><h3>{{ T.tab_basic|default:'基础配置' }}</h3><span class="head-tip">{{ T.tip_config_save|default:'保存后立即写入并热更新,无需重启' }}</span></div>
<div class="cfg-card-body">
<div class="cfg-cols">
<div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_storage_path|default:'数据存储路径' }} <span style="color:#b91c1c">*</span></label>
<div class="cfg-control"><input type="text" class="cfg-input" id="storageDir" placeholder="{{ T.placeholder_storage_path|default:'请输入数据存储路径' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_upload_path|default:'上传目录' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="uploadDir" placeholder="static/upload"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_admin_port|default:'管理端口' }}</label>
<div class="cfg-control"><input type="number" class="cfg-input small" id="adminPort" placeholder="10001"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">ffmpeg</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="ffmpeg" placeholder="ffmpeg"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_font_path|default:'字体路径' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="fontPath" placeholder="static/fonts/..."></div>
</div>
</div>
<div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_host_ip|default:'主机IP' }} <span style="color:#b91c1c">*</span></label>
<div class="cfg-control"><input type="text" class="cfg-input" id="host" placeholder="{{ T.placeholder_host_ip|default:'请输入主机IP' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_auto_proxy|default:'自动流代理' }}</label>
<div class="cfg-control">
<div class="cfg-switch-row">
<label class="switch">
<input type="checkbox" id="autoAddStreamProxy" value="1">
<span class="switch-slider"></span>
</label>
<input type="number" class="cfg-input middle" id="autoAddStreamProxySleep" placeholder="15">
<span class="cfg-switch-label">{{ T.label_delay_seconds|default:'延时(秒)' }}</span>
</div>
</div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_login_captcha|default:'登录验证码' }}</label>
<div class="cfg-control">
<div class="cfg-switch-row">
<label class="switch">
<input type="checkbox" id="isEnableLoginCaptcha" value="1">
<span class="switch-slider"></span>
</label>
<span class="cfg-switch-label">{{ T.switch_show_login_captcha|default:'登录页显示验证码' }}</span>
</div>
</div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_debug_mode|default:'调试模式' }}</label>
<div class="cfg-control">
<div class="cfg-switch-row">
<label class="switch">
<input type="checkbox" id="logDebug" value="1">
<span class="switch-slider"></span>
</label>
<span class="cfg-switch-label">{{ T.switch_debug_log|default:'输出调试日志' }}</span>
</div>
</div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_update_popup|default:'更新提醒' }}</label>
<div class="cfg-control">
<div class="cfg-switch-row">
<label class="switch">
<input type="checkbox" id="isEnableUpdatePopup" value="1">
<span class="switch-slider"></span>
</label>
<span class="cfg-switch-label">{{ T.switch_update_popup|default:'是否开启更新提醒弹框' }}</span>
</div>
</div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">更新检查联网授权</label>
<div class="cfg-control">
<label class="switch"><input type="checkbox" id="updateCheckEnabled" value="1"><span class="switch-slider"></span></label>
<span class="cfg-hint">默认关闭;开启后仅向 MONITOR_UPDATE_ENDPOINT 发送产品、版本和语言</span>
</div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">最小化遥测授权</label>
<div class="cfg-control">
<label class="switch"><input type="checkbox" id="telemetryEnabled" value="1"><span class="switch-slider"></span></label>
<span class="cfg-hint">默认关闭;开启后仅向 MONITOR_TELEMETRY_ENDPOINT 发送产品、版本和心跳序号</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 卡片:流媒体 ZLM -->
<div class="cfg-card" data-card="tab_media">
<div class="cfg-card-head"><h3>{{ T.tab_media|default:'流媒体' }}</h3><span class="head-tip">{{ T.tip_media_zlm|default:'ZLMediaKit 端口与启动路径' }}</span></div>
<div class="cfg-card-body">
<div class="cfg-cols">
<div>
<div class="cfg-form-group">
<label class="cfg-label">HTTP {{ T.label_port|default:'端口' }}</label>
<div class="cfg-control"><input type="number" class="cfg-input small" id="mediaHttpPort"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">RTSP {{ T.label_port|default:'端口' }}</label>
<div class="cfg-control"><input type="number" class="cfg-input small" id="mediaRtspPort"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">RTMP {{ T.label_port|default:'端口' }}</label>
<div class="cfg-control"><input type="number" class="cfg-input small" id="mediaRtmpPort"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_media_secret|default:'API 密钥' }}</label>
<div class="cfg-control"><input type="password" class="cfg-input" id="mediaSecret" disabled placeholder="由运行时密钥或环境变量管理"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">RTMP {{ T.label_proxy|default:'代理' }}</label>
<div class="cfg-control">
<label class="switch"><input type="checkbox" id="isEnableMediaProxyRtmp" value="1"><span class="switch-slider"></span></label>
</div>
</div>
</div>
<div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_media_exe|default:'ZLM 程序路径' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="mediaStartPath" placeholder="zlm/bin.../monitor_zlm.exe"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_media_config|default:'ZLM 配置文件' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="mediaStartConfigPath" placeholder="zlm/bin.../config.ini"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_auto_start_media|default:'随 Monitor 自启' }}</label>
<div class="cfg-control">
<label class="switch"><input type="checkbox" id="autoStartMedia" value="1"><span class="switch-slider"></span></label>
<span class="cfg-hint">{{ T.hint_auto_start_media|default:'调试时可关闭,改在控制面板手动启动' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 卡片:分析 -->
<div class="cfg-card" data-card="tab_analysis">
<div class="cfg-card-head"><h3>{{ T.tab_analysis|default:'分析' }}</h3></div>
<div class="cfg-card-body">
<div class="cfg-cols">
<div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_analysis_fps|default:'解码目标 FPS' }}</label>
<div class="cfg-control"><input type="number" class="cfg-input small" id="analysisTargetFps"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_conf_threshold|default:'置信度阈值' }}</label>
<div class="cfg-control"><input type="number" step="0.01" class="cfg-input small" id="analysisConfThreshold"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_process_mode|default:'进程模式' }}</label>
<div class="cfg-control"><input type="number" class="cfg-input small" id="analysisProcessMode" title="0=线程 1=子进程"><span class="cfg-hint">0=线程 ≥1=子进程</span></div>
</div>
</div>
<div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_shared_inference|default:'共享推理' }}</label>
<div class="cfg-control"><label class="switch"><input type="checkbox" id="analysisSharedInference" value="1"><span class="switch-slider"></span></label></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_inference_workers|default:'推理 Worker 数' }}</label>
<div class="cfg-control"><input type="number" class="cfg-input small" id="analysisInferenceWorkers" min="1" max="8"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 卡片:录像 -->
<div class="cfg-card" data-card="tab_record">
<div class="cfg-card-head"><h3>{{ T.tab_record|default:'录像' }}</h3></div>
<div class="cfg-card-body">
<div class="cfg-cols">
<div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_recording_enabled|default:'启用录像管理' }}</label>
<div class="cfg-control"><label class="switch"><input type="checkbox" id="recordingEnabled" value="1"><span class="switch-slider"></span></label></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_segment_seconds|default:'分段时长(秒)' }}</label>
<div class="cfg-control"><input type="number" class="cfg-input small" id="recordingSegmentSeconds"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_retain_days|default:'保留天数' }}</label>
<div class="cfg-control"><input type="number" class="cfg-input small" id="recordingRetainDays"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_retain_gb|default:'容量上限(GB)' }}</label>
<div class="cfg-control"><input type="number" step="0.1" class="cfg-input small" id="recordingRetainGb"><span class="cfg-hint">0=不限</span></div>
</div>
</div>
</div>
</div>
</div>
<!-- 卡片2GB28181配置 -->
<div class="cfg-card" data-card="tab_gb28181">
<div class="cfg-card-head"><h3>{{ T.tab_gb28181|default:'GB28181 配置' }}</h3><span class="head-tip">{{ T.tip_sip_params|default:'SIP 信令服务器参数' }}</span></div>
<div class="cfg-card-body">
<div class="cfg-cols">
<div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_server_ip|default:'服务器IP' }} <span style="color:#b91c1c">*</span></label>
<div class="cfg-control"><input type="text" class="cfg-input" id="sipServerIp" placeholder="{{ T.placeholder_server_ip|default:'请输入服务器IP' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_server_port|default:'服务器端口' }} <span style="color:#b91c1c">*</span></label>
<div class="cfg-control"><input type="number" class="cfg-input" id="sipServerPort" placeholder="5060"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_sip_nonce|default:'SIP Nonce' }}</label>
<div class="cfg-control"><input type="password" class="cfg-input" id="sipServerNonce" disabled placeholder="由运行时密钥或环境变量管理"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_server_id|default:'服务器ID' }} <span style="color:#b91c1c">*</span></label>
<div class="cfg-control"><input type="text" class="cfg-input" id="sipServerId" placeholder="{{ T.placeholder_server_id|default:'请输入服务器ID' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_server_realm|default:'服务器域' }} <span style="color:#b91c1c">*</span></label>
<div class="cfg-control"><input type="text" class="cfg-input" id="sipServerRealm" placeholder="{{ T.placeholder_server_realm|default:'请输入服务器域' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_server_password|default:'服务器密码' }}</label>
<div class="cfg-control"><input type="password" class="cfg-input" id="sipServerPass" disabled placeholder="由运行时密钥或环境变量管理"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_server_timeout|default:'心跳超时(秒)' }} <span style="color:#b91c1c">*</span></label>
<div class="cfg-control"><input type="number" class="cfg-input" id="sipServerTimeout" placeholder="1800"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_server_expiry|default:'注册过期(秒)' }} <span style="color:#b91c1c">*</span></label>
<div class="cfg-control"><input type="number" class="cfg-input" id="sipServerExpiry" placeholder="3600"></div>
</div>
</div>
<div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_signaling_protocol|default:'信令传输协议' }}</label>
<div class="cfg-control">
<div class="cfg-radio-group">
<input type="radio" name="sipTransferMode" id="sipTransferMode0" value="0">
<label class="cfg-radio-opt" for="sipTransferMode0">UDP</label>
<input type="radio" name="sipTransferMode" id="sipTransferMode1" value="1">
<label class="cfg-radio-opt" for="sipTransferMode1">TCP</label>
</div>
</div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_data_protocol|default:'数据传输协议' }}</label>
<div class="cfg-control">
<div class="cfg-radio-group">
<input type="radio" name="rtpTransferMode" id="rtpTransferMode0" value="0">
<label class="cfg-radio-opt" for="rtpTransferMode0">{{ T.option_udp_passive|default:'UDP 被动' }}</label>
<input type="radio" name="rtpTransferMode" id="rtpTransferMode1" value="1">
<label class="cfg-radio-opt" for="rtpTransferMode1">{{ T.option_tcp_passive|default:'TCP 被动' }}</label>
</div>
</div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_audio_transport_type|default:'音频传输类型' }}</label>
<div class="cfg-control">
<div class="cfg-radio-group">
<input type="radio" name="rtpTransferAudioType" id="rtpTransferAudioType0" value="0">
<label class="cfg-radio-opt" for="rtpTransferAudioType0">{{ T.option_mute|default:'静音' }}</label>
<input type="radio" name="rtpTransferAudioType" id="rtpTransferAudioType1" value="1">
<label class="cfg-radio-opt" for="rtpTransferAudioType1">{{ T.option_original_audio|default:'原始音频' }}</label>
</div>
</div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_auto_invite|default:'自动 INVITE' }}</label>
<div class="cfg-control">
<div class="cfg-switch-row">
<label class="switch">
<input type="checkbox" id="autoInviteAfterRecCateLog" value="1">
<span class="switch-slider"></span>
</label>
<span class="cfg-switch-label">{{ T.switch_auto_invite|default:'收到 Catalog 后自动 INVITE' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 卡片3品牌信息 -->
<div class="cfg-card" data-card="tab_oem">
<div class="cfg-card-head"><h3>{{ T.tab_branding|default:'品牌信息' }}</h3><span class="head-tip">{{ T.tip_oem_branding|default:'OEM 品牌信息' }}</span></div>
<div class="cfg-card-body">
<div class="cfg-cols">
<div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_system_name|default:'系统名称' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="b_name" maxlength="50" placeholder="{{ T.placeholder_system_name|default:'请输入系统名称' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_welcome_msg|default:'欢迎语' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="b_welcome" maxlength="100" placeholder="{{ T.placeholder_welcome_msg|default:'请输入欢迎语' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_logo_url|default:'Logo 地址' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="b_logo_url" placeholder="{{ T.placeholder_logo_url|default:'请输入 Logo 图片地址' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_bottom_name|default:'底部名称' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="b_bottom_name" maxlength="50" placeholder="{{ T.placeholder_bottom_name|default:'请输入底部名称' }}"></div>
</div>
</div>
<div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_author_name|default:'作者名称' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="b_author" maxlength="50" placeholder="{{ T.placeholder_author_name|default:'请输入作者名称' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_author_link|default:'作者链接' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="b_author_link" placeholder="{{ T.placeholder_author_link|default:'请输入作者主页链接' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_version_download_link|default:'版本下载链接' }}</label>
<div class="cfg-control"><input type="text" class="cfg-input" id="b_check_version_download_url" placeholder="{{ T.placeholder_version_download_link|default:'版本检查与下载地址' }}"></div>
</div>
<div class="cfg-form-group">
<label class="cfg-label">{{ T.label_show_author|default:'显示作者' }}</label>
<div class="cfg-control">
<div class="cfg-switch-row">
<label class="switch">
<input type="checkbox" id="b_is_show_author">
<span class="switch-slider"></span>
</label>
<span class="cfg-switch-label">{{ T.switch_show_author_footer|default:'页面底部显示作者信息' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部操作栏 -->
<div class="cfg-actions">
<button class="cfg-btn" onclick="loadAllConfig()">{{ T.btn_refresh|default:'刷新' }}</button>
<button class="cfg-btn cfg-btn-primary" id="btnSave" onclick="saveCurrentTab()">{{ T.btn_save|default:'保存' }}</button>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
(function () {
var currentTab = 'tab_basic';
var hasUnsavedChange = false;
window.switchCfgTab = function(tabId) {
currentTab = tabId;
document.querySelectorAll('.cfg-tab').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabId);
});
document.querySelectorAll('.cfg-card').forEach(function(card) {
card.classList.toggle('active', card.getAttribute('data-card') === tabId);
});
hasUnsavedChange = false;
};
function pick(d, flat, obj, sub) {
if (d == null) return '';
if (d[flat] !== undefined && d[flat] !== null) return d[flat];
if (obj && sub && d[obj] && d[obj][sub] !== undefined && d[obj][sub] !== null) return d[obj][sub];
return '';
}
function setVal(id, v) { var el = document.getElementById(id); if (el) el.value = (v === null || v === undefined) ? '' : v; }
function setChecked(id, v) { var el = document.getElementById(id); if (el) el.checked = (v === 1 || v === '1' || v === true); }
function setRadio(name, v) {
var radios = document.getElementsByName(name);
var value = (v === null || v === undefined) ? '' : String(v);
for (var i = 0; i < radios.length; i++) radios[i].checked = (radios[i].value === value);
}
function val(id) { var el = document.getElementById(id); return el ? el.value : ''; }
function checked(id) { var el = document.getElementById(id); return el && el.checked ? 1 : 0; }
function toInt(id, def) { var n = parseInt(val(id), 10); return isNaN(n) ? (def || 0) : n; }
function radioVal(name) {
var radios = document.getElementsByName(name);
for (var i = 0; i < radios.length; i++) if (radios[i].checked) return parseInt(radios[i].value, 10);
return 0;
}
function markDirty() {
if (!hasUnsavedChange) {
hasUnsavedChange = true;
}
}
document.addEventListener('input', function(e) {
if (e.target.matches('.cfg-card.active input, .cfg-card.active select, .cfg-card.active textarea')) markDirty();
});
document.addEventListener('change', function(e) {
if (e.target.matches('.cfg-card.active input, .cfg-card.active select, .cfg-card.active textarea')) markDirty();
});
window.loadAllConfig = function () {
Api.get('/system/openConfig').then(function (res) {
if (res && res.code === 1000) {
fillForm(res.data || res.config || {}, res.branding || {});
} else {
showToast((res && res.msg) || _t('toast_config_load_failed', '配置加载失败'), 'error');
}
});
};
function fillForm(cfg, brand) {
setVal('host', pick(cfg, 'host'));
setVal('storageDir', pick(cfg, 'storageDir'));
setVal('uploadDir', pick(cfg, 'uploadDir'));
setVal('adminPort', pick(cfg, 'adminPort'));
setVal('ffmpeg', pick(cfg, 'ffmpeg'));
setVal('fontPath', pick(cfg, 'fontPath'));
setChecked('autoAddStreamProxy', pick(cfg, 'autoAddStreamProxy'));
setVal('autoAddStreamProxySleep', pick(cfg, 'autoAddStreamProxySleep'));
setChecked('isEnableLoginCaptcha', pick(cfg, 'isEnableLoginCaptcha'));
setChecked('logDebug', pick(cfg, 'logDebug'));
setChecked('isEnableUpdatePopup', pick(cfg, 'isEnableUpdatePopup'));
setChecked('updateCheckEnabled', pick(cfg, 'updateCheckEnabled'));
setChecked('telemetryEnabled', pick(cfg, 'telemetryEnabled'));
setVal('mediaHttpPort', pick(cfg, 'mediaHttpPort'));
setVal('mediaRtspPort', pick(cfg, 'mediaRtspPort'));
setVal('mediaRtmpPort', pick(cfg, 'mediaRtmpPort'));
setVal('mediaSecret', pick(cfg, 'mediaSecret'));
setChecked('isEnableMediaProxyRtmp', pick(cfg, 'isEnableMediaProxyRtmp'));
setVal('mediaStartPath', pick(cfg, 'mediaStartPath'));
setVal('mediaStartConfigPath', pick(cfg, 'mediaStartConfigPath'));
setChecked('autoStartMedia', pick(cfg, 'autoStartMedia'));
setVal('analysisTargetFps', pick(cfg, 'analysisTargetFps'));
setVal('analysisConfThreshold', pick(cfg, 'analysisConfThreshold'));
setVal('analysisProcessMode', pick(cfg, 'analysisProcessMode'));
setChecked('analysisSharedInference', pick(cfg, 'analysisSharedInference'));
setVal('analysisInferenceWorkers', pick(cfg, 'analysisInferenceWorkers'));
setChecked('recordingEnabled', pick(cfg, 'recordingEnabled'));
setVal('recordingSegmentSeconds', pick(cfg, 'recordingSegmentSeconds'));
setVal('recordingRetainDays', pick(cfg, 'recordingRetainDays'));
setVal('recordingRetainGb', pick(cfg, 'recordingRetainGb'));
setVal('sipServerIp', pick(cfg, 'sipServerIp', 'sipServer', 'sipServerIp'));
setVal('sipServerPort', pick(cfg, 'sipServerPort', 'sipServer', 'sipServerPort'));
setVal('sipServerNonce', pick(cfg, 'sipServerNonce', 'sipServer', 'sipServerNonce'));
setVal('sipServerId', pick(cfg, 'sipServerId', 'sipServer', 'sipServerId'));
setVal('sipServerRealm', pick(cfg, 'sipServerRealm', 'sipServer', 'sipServerRealm'));
setVal('sipServerPass', pick(cfg, 'sipServerPass', 'sipServer', 'sipServerPass'));
setVal('sipServerTimeout', pick(cfg, 'sipServerTimeout', 'sipServer', 'sipServerTimeout'));
setVal('sipServerExpiry', pick(cfg, 'sipServerExpiry', 'sipServer', 'sipServerExpiry'));
setRadio('sipTransferMode', pick(cfg, 'sipTransferMode', 'sipServer', 'sipTransferMode'));
setRadio('rtpTransferMode', pick(cfg, 'rtpTransferMode', 'sipServer', 'rtpTransferMode'));
setRadio('rtpTransferAudioType', pick(cfg, 'rtpTransferAudioType', 'sipServer', 'rtpTransferAudioType'));
setChecked('autoInviteAfterRecCateLog', pick(cfg, 'autoInviteAfterRecCateLog', 'sipServer', 'autoInviteAfterRecCateLog'));
setVal('b_name', brand.name);
setVal('b_welcome', brand.welcome);
setVal('b_logo_url', brand.logo_url);
setVal('b_bottom_name', brand.bottom_name);
setVal('b_author', brand.author);
setVal('b_author_link', brand.author_link);
setVal('b_check_version_download_url', brand.check_version_download_url);
setChecked('b_is_show_author', brand.is_show_author);
hasUnsavedChange = false;
}
window.saveCurrentTab = function () {
if (currentTab === 'tab_oem') saveOEM();
else saveConfig();
};
function saveConfig() {
var params = {
host: val('host').trim(),
storageDir: val('storageDir').trim(),
uploadDir: val('uploadDir').trim(),
adminPort: toInt('adminPort', 10001),
ffmpeg: val('ffmpeg').trim(),
fontPath: val('fontPath').trim(),
autoAddStreamProxy: checked('autoAddStreamProxy'),
autoAddStreamProxySleep: toInt('autoAddStreamProxySleep', 15),
isEnableLoginCaptcha: checked('isEnableLoginCaptcha'),
logDebug: checked('logDebug'),
isEnableUpdatePopup: checked('isEnableUpdatePopup'),
updateCheckEnabled: checked('updateCheckEnabled'),
telemetryEnabled: checked('telemetryEnabled'),
mediaHttpPort: toInt('mediaHttpPort', 10002),
mediaRtspPort: toInt('mediaRtspPort', 10554),
mediaRtmpPort: toInt('mediaRtmpPort', 10935),
mediaSecret: val('mediaSecret').trim(),
isEnableMediaProxyRtmp: checked('isEnableMediaProxyRtmp'),
mediaStartPath: val('mediaStartPath').trim(),
mediaStartConfigPath: val('mediaStartConfigPath').trim(),
autoStartMedia: checked('autoStartMedia'),
analysisTargetFps: toInt('analysisTargetFps', 5),
analysisConfThreshold: parseFloat(val('analysisConfThreshold')) || 0.4,
analysisProcessMode: toInt('analysisProcessMode', 1),
analysisSharedInference: checked('analysisSharedInference'),
analysisInferenceWorkers: toInt('analysisInferenceWorkers', 2),
recordingEnabled: checked('recordingEnabled'),
recordingSegmentSeconds: toInt('recordingSegmentSeconds', 600),
recordingRetainDays: toInt('recordingRetainDays', 7),
recordingRetainGb: parseFloat(val('recordingRetainGb')) || 0,
sipServerIp: val('sipServerIp').trim(),
sipServerPort: toInt('sipServerPort', 5060),
sipServerNonce: val('sipServerNonce').trim(),
sipServerId: val('sipServerId').trim(),
sipServerRealm: val('sipServerRealm').trim(),
sipServerPass: val('sipServerPass'),
sipServerTimeout: toInt('sipServerTimeout', 1800),
sipServerExpiry: toInt('sipServerExpiry', 3600),
sipTransferMode: radioVal('sipTransferMode'),
rtpTransferMode: radioVal('rtpTransferMode'),
rtpTransferAudioType: radioVal('rtpTransferAudioType'),
autoInviteAfterRecCateLog: checked('autoInviteAfterRecCateLog')
};
confirmDialog(_t('confirm_save_config', '确定保存当前配置吗?'), function () {
var btn = document.getElementById('btnSave');
btn.disabled = true;
var orig = btn.textContent;
btn.textContent = _t('saving', '保存中...');
Api.post('/system/openConfig', params).then(function (res) {
btn.disabled = false;
btn.textContent = orig;
if (res && res.code === 1000) {
showToast(res.msg || _t('syscfg_save_success', '保存成功,配置已生效'), 'success');
hasUnsavedChange = false;
} else {
showToast((res && res.msg) || _t('save_failed', '保存失败'), 'error');
}
}).catch(function () {
btn.disabled = false;
btn.textContent = orig;
});
});
}
function saveOEM() {
var data = {
name: val('b_name').trim(),
welcome: val('b_welcome').trim(),
logo_url: val('b_logo_url').trim(),
bottom_name: val('b_bottom_name').trim(),
author: val('b_author').trim(),
author_link: val('b_author_link').trim(),
check_version_download_url: val('b_check_version_download_url').trim(),
is_show_author: checked('b_is_show_author') ? true : false
};
var btn = document.getElementById('btnSave');
btn.disabled = true;
var orig = btn.textContent;
btn.textContent = _t('saving', '保存中...');
Api.post('/system/openSaveSettings', data).then(function (res) {
btn.disabled = false;
btn.textContent = orig;
if (res && res.code === 1000) {
showToast(res.msg || _t('toast_branding_saved', '品牌信息保存成功'), 'success');
hasUnsavedChange = false;
} else {
showToast((res && res.msg) || _t('save_failed', '保存失败'), 'error');
}
}).catch(function () {
btn.disabled = false;
btn.textContent = orig;
});
}
document.addEventListener('DOMContentLoaded', function () { loadAllConfig(); });
})();
</script>
{% endblock %}