1527 lines
68 KiB
HTML
1527 lines
68 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
<title>海康 H5 视频监控</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0a0e1a;
|
||
--surface: #111827;
|
||
--surface2: #0f1623;
|
||
--border: #1e3a5f;
|
||
--accent: #0ea5e9;
|
||
--accent-dim: #0369a1;
|
||
--accent-glow: rgba(14,165,233,.12);
|
||
--text: #e2e8f0;
|
||
--text-muted: #64748b;
|
||
--text-dim: #94a3b8;
|
||
--green: #22c55e;
|
||
--red: #ef4444;
|
||
--yellow: #f59e0b;
|
||
--radius: 8px;
|
||
--btn-h: 38px;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
||
html, body {
|
||
height: 100%; background: var(--bg); color: var(--text);
|
||
font-family: 'PingFang SC','Microsoft YaHei',sans-serif;
|
||
font-size: 14px; overflow: hidden;
|
||
}
|
||
#root { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
|
||
|
||
/* ── 顶栏 ── */
|
||
.topbar {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 0 12px; height: 46px; flex-shrink: 0;
|
||
background: var(--surface2); border-bottom: 1px solid var(--border);
|
||
}
|
||
.topbar-brand { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700; }
|
||
.topbar-brand .logo {
|
||
width: 26px; height: 26px; border-radius: 6px;
|
||
background: linear-gradient(135deg, var(--accent), #3b82f6);
|
||
display: flex; align-items: center; justify-content: center; font-size: 13px;
|
||
}
|
||
.topbar-right { display: flex; align-items: center; gap: 6px; }
|
||
.conn-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--red); box-shadow: 0 0 5px var(--red); }
|
||
.conn-dot.on { background: var(--green); box-shadow: 0 0 5px var(--green); }
|
||
.conn-label { font-size: 11px; color: var(--text-muted); }
|
||
|
||
/* ── 播放器 ── */
|
||
#player { width: 100%; background: #000; flex-shrink: 0; position: relative; }
|
||
.ov-btns { position: absolute; bottom: 6px; right: 6px; display: flex; gap: 4px; }
|
||
.ov-btn {
|
||
width: 30px; height: 30px; border-radius: 50%; border: none;
|
||
background: rgba(0,0,0,.55); color: #fff; cursor: pointer; font-size: 13px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
backdrop-filter: blur(4px); transition: background .15s;
|
||
}
|
||
.ov-btn:hover { background: rgba(14,165,233,.75); }
|
||
|
||
/* ── 分屏栏 ── */
|
||
.split-bar {
|
||
display: flex; align-items: center; gap: 5px; flex-shrink: 0;
|
||
padding: 5px 12px; background: var(--surface2); border-bottom: 1px solid var(--border);
|
||
}
|
||
.split-label { font-size: 11px; color: var(--text-muted); }
|
||
.sp-pill {
|
||
padding: 2px 9px; border-radius: 99px; font-size: 11px; cursor: pointer;
|
||
border: 1px solid var(--border); background: var(--surface); color: var(--text-muted);
|
||
transition: all .15s;
|
||
}
|
||
.sp-pill.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||
.split-bar-r { margin-left: auto; display: flex; gap: 5px; }
|
||
.ico-btn {
|
||
width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--border);
|
||
background: var(--surface); color: var(--text-dim); cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center; font-size: 12px;
|
||
transition: all .15s;
|
||
}
|
||
.ico-btn:hover, .ico-btn.hi { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); }
|
||
.ico-btn:active { transform: scale(.9); }
|
||
|
||
/* ── 标签页 ── */
|
||
.tabs {
|
||
display: flex; flex-shrink: 0;
|
||
background: var(--surface2); border-bottom: 1px solid var(--border);
|
||
}
|
||
.tab {
|
||
flex: 1; padding: 9px 2px; text-align: center; font-size: 11px; font-weight: 600;
|
||
color: var(--text-muted); cursor: pointer; letter-spacing: .2px;
|
||
border-bottom: 2px solid transparent; transition: all .15s;
|
||
}
|
||
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||
|
||
/* ── 可滚动面板 ── */
|
||
.panel { flex: 1; overflow-y: auto; background: var(--bg); -webkit-overflow-scrolling: touch; }
|
||
.panel::-webkit-scrollbar { width: 2px; }
|
||
.panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||
.pane { display: none; }
|
||
.pane.active { display: block; }
|
||
|
||
/* ── 通用区块 ── */
|
||
.sec { padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
||
.sec-title {
|
||
font-size: 10px; color: var(--text-muted); text-transform: uppercase;
|
||
letter-spacing: .8px; font-weight: 600; margin-bottom: 7px;
|
||
}
|
||
|
||
/* ── 搜索框 ── */
|
||
.search-wrap { position: relative; padding: 8px 12px 6px; }
|
||
.search-inp {
|
||
width: 100%; background: var(--surface); border: 1px solid var(--border);
|
||
border-radius: var(--radius); color: var(--text); padding: 7px 10px 7px 28px;
|
||
font-size: 12px; outline: none;
|
||
}
|
||
.search-inp:focus { border-color: var(--accent); }
|
||
.search-icon { position: absolute; left: 20px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 13px; pointer-events: none; }
|
||
|
||
/* ── 面包屑 ── */
|
||
.breadcrumb {
|
||
display: flex; align-items: center; flex-wrap: wrap; gap: 3px;
|
||
padding: 5px 12px 6px; min-height: 28px; font-size: 11px;
|
||
color: var(--text-muted); border-bottom: 1px solid var(--border);
|
||
}
|
||
.bc-item { color: var(--accent); cursor: pointer; }
|
||
.bc-item:hover { text-decoration: underline; }
|
||
.bc-sep { color: var(--text-muted); }
|
||
.bc-cur { color: var(--text-dim); }
|
||
|
||
/* ── 返回按钮 ── */
|
||
.back-row {
|
||
display: flex; align-items: center; gap: 6px;
|
||
padding: 7px 12px; font-size: 12px; color: var(--accent);
|
||
cursor: pointer; border-bottom: 1px solid var(--border);
|
||
}
|
||
.back-row:hover { background: var(--accent-glow); }
|
||
|
||
/* ── 分组标题 ── */
|
||
.group-title {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 5px 12px 3px; font-size: 10px; font-weight: 700;
|
||
color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px;
|
||
}
|
||
.group-count {
|
||
background: var(--surface); border: 1px solid var(--border);
|
||
border-radius: 99px; padding: 1px 6px; font-size: 9px; color: var(--text-muted);
|
||
}
|
||
|
||
/* ── 区域条目 ── */
|
||
.region-item {
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 9px 12px; cursor: pointer;
|
||
border-bottom: 1px solid rgba(30,58,95,.4);
|
||
transition: background .15s;
|
||
}
|
||
.region-item:hover { background: var(--surface); }
|
||
.region-icon { font-size: 15px; flex-shrink: 0; }
|
||
.region-name { flex: 1; font-size: 13px; font-weight: 500; }
|
||
.region-arrow { font-size: 11px; color: var(--text-muted); }
|
||
|
||
/* ── 摄像头条目 ── */
|
||
.cam-item {
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 8px 12px; border-bottom: 1px solid rgba(30,58,95,.4);
|
||
transition: background .15s;
|
||
}
|
||
.cam-item:hover { background: var(--surface); }
|
||
.cam-item.active { background: var(--accent-glow); border-left: 2px solid var(--accent); }
|
||
.cam-icon { font-size: 14px; flex-shrink: 0; }
|
||
.cam-info { flex: 1; min-width: 0; }
|
||
.cam-name { font-size: 12px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.cam-code { font-size: 10px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: monospace; }
|
||
.cam-badge { font-size: 9px; padding: 1px 5px; border-radius: 99px; flex-shrink: 0; }
|
||
.b-on { background: rgba(34,197,94,.15); color: var(--green); }
|
||
.b-off { background: rgba(239,68,68,.15); color: var(--red); }
|
||
.b-unk { background: rgba(100,116,139,.15); color: var(--text-muted); }
|
||
.cam-actions { display: flex; gap: 4px; flex-shrink: 0; }
|
||
.ca-btn {
|
||
width: 26px; height: 26px; border-radius: 5px; border: 1px solid var(--border);
|
||
background: var(--surface); color: var(--text-dim); cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center; font-size: 12px;
|
||
transition: all .15s;
|
||
}
|
||
.ca-btn.play:hover { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||
.ca-btn.cap:hover { background: #7c3aed; border-color: #7c3aed; color: #fff; }
|
||
|
||
/* ── 输入 / 选择 ── */
|
||
.inp {
|
||
width: 100%; background: var(--surface); border: 1px solid var(--border);
|
||
border-radius: var(--radius); color: var(--text); padding: 7px 10px;
|
||
font-size: 12px; outline: none; transition: border .2s;
|
||
}
|
||
.inp:focus { border-color: var(--accent); }
|
||
.inp[readonly] { color: var(--text-dim); cursor: default; }
|
||
.sel {
|
||
width: 100%; background: var(--surface); border: 1px solid var(--border);
|
||
border-radius: var(--radius); color: var(--text); padding: 7px 9px;
|
||
font-size: 12px; outline: none;
|
||
}
|
||
.field { margin-bottom: 7px; }
|
||
.field-lbl { font-size: 10px; color: var(--text-muted); margin-bottom: 3px; }
|
||
.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
|
||
|
||
/* ── 按钮 ── */
|
||
.btn-row { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
|
||
.btn {
|
||
height: var(--btn-h); padding: 0 12px; border-radius: var(--radius); border: none;
|
||
cursor: pointer; font-size: 11px; font-weight: 600;
|
||
display: flex; align-items: center; gap: 4px; white-space: nowrap;
|
||
transition: opacity .15s, transform .1s;
|
||
}
|
||
.btn:active { opacity: .75; transform: scale(.95); }
|
||
.btn-primary { background: var(--accent); color: #fff; }
|
||
.btn-default { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
|
||
.btn-danger { background: var(--red); color: #fff; }
|
||
.btn-warn { background: var(--yellow); color: #000; }
|
||
.btn-ghost { background: transparent; color: var(--accent); border: 1px solid var(--accent); }
|
||
.btn-sm { height: 30px; padding: 0 9px; font-size: 11px; }
|
||
.btn-full { width: 100%; justify-content: center; }
|
||
|
||
/* ── 音量 ── */
|
||
.vol-row { display: flex; align-items: center; gap: 8px; }
|
||
.vol-icon { font-size: 16px; cursor: pointer; flex-shrink: 0; }
|
||
.vol-slider {
|
||
flex: 1; -webkit-appearance: none; height: 3px;
|
||
border-radius: 2px; background: var(--border); outline: none;
|
||
}
|
||
.vol-slider::-webkit-slider-thumb {
|
||
-webkit-appearance: none; width: 14px; height: 14px;
|
||
border-radius: 50%; background: var(--accent); cursor: pointer;
|
||
}
|
||
.vol-val { min-width: 24px; text-align: right; font-size: 11px; color: var(--text-muted); }
|
||
|
||
/* ── 速率胶囊 ── */
|
||
.rate-wrap { display: flex; flex-wrap: wrap; gap: 4px; }
|
||
.rate-pill {
|
||
padding: 3px 8px; border-radius: 99px; border: 1px solid var(--border);
|
||
background: var(--surface); color: var(--text-muted); cursor: pointer; font-size: 11px;
|
||
transition: all .15s;
|
||
}
|
||
.rate-pill.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||
|
||
/* ── 抓图图库 ── */
|
||
.gallery { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 8px; }
|
||
.g-card { border-radius: 7px; overflow: hidden; border: 1px solid var(--border); background: var(--surface); }
|
||
.g-card:hover { border-color: var(--accent); }
|
||
.g-card img { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block; background: #000; cursor: pointer; }
|
||
.g-info { padding: 4px 6px; }
|
||
.g-name { font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.g-time { font-size: 9px; color: var(--text-muted); font-family: monospace; }
|
||
.g-actions { display: flex; gap: 3px; padding: 0 6px 5px; }
|
||
.g-act {
|
||
flex: 1; font-size: 10px; padding: 2px 0; text-align: center;
|
||
border: 1px solid var(--border); border-radius: 4px;
|
||
background: var(--bg); color: var(--text-muted); cursor: pointer; text-decoration: none;
|
||
transition: all .15s;
|
||
}
|
||
.g-act:hover { border-color: var(--accent); color: var(--accent); }
|
||
.g-act.del:hover { border-color: var(--red); color: var(--red); }
|
||
|
||
/* ── 定时任务卡片 ── */
|
||
.task-card {
|
||
border: 1px solid var(--border); border-radius: 7px;
|
||
background: var(--surface); padding: 9px 10px; margin-bottom: 6px;
|
||
}
|
||
.task-card.running { border-color: var(--green); }
|
||
.task-card.error { border-color: var(--yellow); }
|
||
.task-hd { display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px; }
|
||
.task-name { font-size: 12px; font-weight: 600; }
|
||
.task-st { font-size: 10px; padding: 2px 6px; border-radius: 99px; }
|
||
.task-st.RUNNING { background: rgba(34,197,94,.15); color: var(--green); }
|
||
.task-st.STOPPED { background: rgba(100,116,139,.15); color: var(--text-muted); }
|
||
.task-st.ERROR { background: rgba(245,158,11,.15); color: var(--yellow); }
|
||
.task-meta { font-size: 10px; color: var(--text-muted); line-height: 1.8; }
|
||
.task-thumb { width: 100%; border-radius: 4px; margin-top: 5px; aspect-ratio: 16/9; object-fit: cover; background: #000; cursor: pointer; border: 1px solid var(--border); }
|
||
.task-btns { display: flex; gap: 4px; margin-top: 6px; }
|
||
.t-btn {
|
||
flex: 1; font-size: 10px; padding: 4px 0; text-align: center;
|
||
border: 1px solid var(--border); border-radius: 5px;
|
||
background: var(--bg); color: var(--text-dim); cursor: pointer; transition: all .15s;
|
||
}
|
||
.t-btn.start:hover { border-color: var(--green); color: var(--green); }
|
||
.t-btn.stop:hover { border-color: var(--yellow); color: var(--yellow); }
|
||
.t-btn.del:hover { border-color: var(--red); color: var(--red); }
|
||
|
||
/* ── 灯箱 ── */
|
||
#lightbox {
|
||
display: none; position: fixed; inset: 0; z-index: 9000;
|
||
background: rgba(0,0,0,.88); align-items: center; justify-content: center;
|
||
flex-direction: column; gap: 10px;
|
||
}
|
||
#lightbox.show { display: flex; }
|
||
#lightbox img { max-width: 92vw; max-height: 78vh; border-radius: 7px; }
|
||
#lb-info { color: var(--text-dim); font-size: 12px; }
|
||
#lb-close { position: fixed; top: 16px; right: 16px; font-size: 26px; color: #fff; cursor: pointer; z-index: 9001; }
|
||
|
||
/* ── 状态栏 ── */
|
||
.statusbar {
|
||
background: var(--surface2); border-top: 1px solid var(--border);
|
||
padding: 4px 12px; flex-shrink: 0; display: flex; align-items: center; gap: 6px;
|
||
}
|
||
.sb-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0; }
|
||
.sb-dot.playing { background: var(--green); animation: pulse 1.5s infinite; }
|
||
.sb-dot.error { background: var(--red); }
|
||
.sb-text { font-size: 11px; color: var(--text-muted); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: monospace; }
|
||
.sb-rate { font-size: 11px; color: var(--yellow); font-weight: 700; }
|
||
|
||
/* ── Toast ── */
|
||
#toast {
|
||
position: fixed; bottom: 56px; left: 50%; transform: translateX(-50%);
|
||
background: #1e293b; border: 1px solid var(--border); border-radius: var(--radius);
|
||
padding: 7px 16px; font-size: 12px; color: var(--text); pointer-events: none;
|
||
display: none; z-index: 9999; white-space: nowrap; box-shadow: 0 4px 20px rgba(0,0,0,.5);
|
||
}
|
||
#toast.show { display: block; animation: fadeup .2s; }
|
||
|
||
/* ── 工具类 ── */
|
||
.spin {
|
||
width: 14px; height: 14px; border: 2px solid var(--border);
|
||
border-top-color: var(--accent); border-radius: 50%;
|
||
animation: spin .6s linear infinite; display: inline-block; vertical-align: middle;
|
||
}
|
||
.empty-tip { text-align: center; padding: 28px 12px; color: var(--text-muted); font-size: 12px; line-height: 2; }
|
||
.empty-tip .ei { font-size: 26px; display: block; }
|
||
|
||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
|
||
@keyframes spin { to{transform:rotate(360deg)} }
|
||
@keyframes fadeup { from{opacity:0;transform:translateX(-50%) translateY(6px)} to{opacity:1;transform:translateX(-50%)} }
|
||
::-webkit-media-controls { display: none !important; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="root">
|
||
|
||
<!-- 顶栏 -->
|
||
<div class="topbar">
|
||
<div class="topbar-brand">
|
||
<div class="logo">📹</div>
|
||
海康 H5 监控
|
||
</div>
|
||
<div class="topbar-right">
|
||
<div class="conn-dot" id="conn-dot"></div>
|
||
<span class="conn-label" id="conn-label">未连接</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 播放器 -->
|
||
<div id="player">
|
||
<div class="ov-btns">
|
||
<button class="ov-btn" title="整体全屏" onclick="wholeFullScreen()">⛶</button>
|
||
<button class="ov-btn" title="单窗口全屏" onclick="singleFullScreen()">⊡</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分屏栏 -->
|
||
<div class="split-bar">
|
||
<span class="split-label">分屏</span>
|
||
<span class="sp-pill active" id="sp1" onclick="setSplit(1)">1×1</span>
|
||
<span class="sp-pill" id="sp2" onclick="setSplit(2)">2×2</span>
|
||
<span class="sp-pill" id="sp3" onclick="setSplit(3)">3×3</span>
|
||
<span class="sp-pill" id="sp4" onclick="setSplit(4)">4×4</span>
|
||
<div class="split-bar-r">
|
||
<button class="ico-btn" id="mode-btn" onclick="cycleMode()" title="普通/高级模式">MSE</button>
|
||
<button class="ico-btn" title="全部停止" onclick="stopAllPlay()">⏹</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 标签页导航 -->
|
||
<div class="tabs">
|
||
<div class="tab active" id="tab-region" onclick="switchTab('region')">🗂 区域</div>
|
||
<div class="tab" id="tab-preview" onclick="switchTab('preview')">▶ 预览</div>
|
||
<div class="tab" id="tab-playback" onclick="switchTab('playback')">⏪ 回放</div>
|
||
<div class="tab" id="tab-capture" onclick="switchTab('capture')">📸 抓图</div>
|
||
<div class="tab" id="tab-more" onclick="switchTab('more')">⚙ 更多</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════ 面板内容 ═══════════════════ -->
|
||
<div class="panel">
|
||
|
||
<!-- ══ 区域摄像头 ══ -->
|
||
<div class="pane active" id="pane-region">
|
||
<!-- 搜索框 -->
|
||
<div class="search-wrap">
|
||
<span class="search-icon">🔍</span>
|
||
<input class="search-inp" type="search" id="cam-search"
|
||
placeholder="搜索摄像头名称或编号…" oninput="filterInCurrentView(this.value)">
|
||
</div>
|
||
<!-- 面包屑 -->
|
||
<div class="breadcrumb" id="breadcrumb">
|
||
<span class="bc-cur">根节点</span>
|
||
</div>
|
||
<!-- 区域/摄像头列表 -->
|
||
<div id="region-list">
|
||
<div class="empty-tip"><div class="spin"></div><br>加载中…</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══ 预览控制 ══ -->
|
||
<div class="pane" id="pane-preview">
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">当前摄像头</div>
|
||
<div class="field">
|
||
<div class="field-lbl">监控点编号(点击区域列表自动填入)</div>
|
||
<input class="inp" id="cur-code" placeholder="cameraIndexCode" readonly>
|
||
</div>
|
||
<div class="row2">
|
||
<div class="field">
|
||
<div class="field-lbl">码流</div>
|
||
<select class="sel" id="stream-type">
|
||
<option value="0">主码流</option>
|
||
<option value="1">子码流</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-lbl">协议</div>
|
||
<select class="sel" id="stream-proto">
|
||
<option value="ws">WS(内网推荐)</option>
|
||
<option value="wss">WSS(加密)</option>
|
||
<option value="rtsp">RTSP</option>
|
||
<option value="hls">HLS</option>
|
||
<option value="flv">FLV</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-lbl">传输</div>
|
||
<select class="sel" id="trans-mode">
|
||
<option value="1">TCP</option>
|
||
<option value="0">UDP</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-primary" onclick="startPreview()">▶ 开始预览</button>
|
||
<button class="btn btn-danger" onclick="stopPlay()">■ 停止</button>
|
||
<button class="btn btn-default btn-sm" onclick="stopAllPlay()">全部停止</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">播放地址(调试用)</div>
|
||
<div style="display:flex;gap:6px;">
|
||
<input class="inp" id="wss-url" readonly placeholder="预览后自动填入">
|
||
<button class="btn btn-default btn-sm" onclick="copyText('wss-url')">📋</button>
|
||
</div>
|
||
<div style="margin-top:6px;font-size:11px;color:var(--yellow);line-height:1.6;">
|
||
⚠ 若使用 WSS 报 WebSocket 连接失败,请先在浏览器中访问<br>
|
||
<span style="color:var(--accent);font-family:monospace;">https://10.60.49.82</span> 和
|
||
<span style="color:var(--accent);font-family:monospace;">https://10.60.49.82:6014</span><br>
|
||
手动接受自签名证书,或改用 <b>WS</b> 协议(内网无需证书)。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">声音</div>
|
||
<div class="vol-row">
|
||
<span class="vol-icon" id="vol-icon" onclick="toggleMute()">🔇</span>
|
||
<input class="vol-slider" type="range" id="vol-slider" min="0" max="100" value="50"
|
||
oninput="setVolume(this.value)">
|
||
<span class="vol-val" id="vol-val">50</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">对讲</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-default btn-sm" onclick="talkStart()">🎙 开始</button>
|
||
<button class="btn btn-default btn-sm" onclick="talkStop()">✕ 停止</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">后端抓图(调用 API)</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-default btn-sm" onclick="captureImage(selCode, selName, null)">📸 API抓图</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /pane-preview -->
|
||
|
||
<!-- ══ 回放控制 ══ -->
|
||
<div class="pane" id="pane-playback">
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">回放参数</div>
|
||
<div class="field">
|
||
<div class="field-lbl">监控点编号</div>
|
||
<input class="inp" id="pb-code" placeholder="cameraIndexCode" readonly>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-lbl">开始时间</div>
|
||
<input class="inp" type="datetime-local" id="pb-start" step="1">
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-lbl">结束时间</div>
|
||
<input class="inp" type="datetime-local" id="pb-end" step="1">
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-primary" onclick="playbackStart()">▶ 开始回放</button>
|
||
<button class="btn btn-default" onclick="reversekStart()">◀ 倒放</button>
|
||
<button class="btn btn-danger" onclick="stopPlay()">■ 停止</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">回放操作</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-default btn-sm" onclick="playbackPause()">⏸ 暂停</button>
|
||
<button class="btn btn-default btn-sm" onclick="playbackResume(true)">▶ 正向</button>
|
||
<button class="btn btn-default btn-sm" onclick="playbackResume(false)">◀ 倒向</button>
|
||
<button class="btn btn-default btn-sm" onclick="changeMode()">⇆ 切换</button>
|
||
</div>
|
||
<div class="btn-row" style="margin-top:5px;">
|
||
<button class="btn btn-default btn-sm" onclick="frameBack()">|◀ 单帧退</button>
|
||
<button class="btn btn-default btn-sm" onclick="frameForward()">▶| 单帧进</button>
|
||
<button class="btn btn-default btn-sm" onclick="playbackSlow()">🐢 慢放</button>
|
||
<button class="btn btn-default btn-sm" onclick="playbackFast()">🐇 快放</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">定位(Seek)</div>
|
||
<input class="inp" type="datetime-local" id="pb-seek" step="1" style="margin-bottom:6px;">
|
||
<button class="btn btn-primary btn-sm" onclick="seekTo()">📍 跳转</button>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">播放速率</div>
|
||
<div class="rate-wrap" id="rate-pills">
|
||
<span class="rate-pill" data-r="-8" onclick="setRate(-8)">-8×</span>
|
||
<span class="rate-pill" data-r="-4" onclick="setRate(-4)">-4×</span>
|
||
<span class="rate-pill" data-r="-2" onclick="setRate(-2)">-2×</span>
|
||
<span class="rate-pill active" data-r="1" onclick="setRate(1)">1×</span>
|
||
<span class="rate-pill" data-r="2" onclick="setRate(2)">2×</span>
|
||
<span class="rate-pill" data-r="4" onclick="setRate(4)">4×</span>
|
||
<span class="rate-pill" data-r="8" onclick="setRate(8)">8×</span>
|
||
<span class="rate-pill" data-r="16" onclick="setRate(16)">16×</span>
|
||
<span class="rate-pill" data-r="32" onclick="setRate(32)">32×</span>
|
||
<span class="rate-pill" data-r="64" onclick="setRate(64)">64×</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">WSS 回放地址(调试用)</div>
|
||
<div style="display:flex;gap:6px;">
|
||
<input class="inp" id="pb-wss-url" readonly placeholder="回放后自动填入">
|
||
<button class="btn btn-default btn-sm" onclick="copyText('pb-wss-url')">📋</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /pane-playback -->
|
||
|
||
<!-- ══ 抓图图库 ══ -->
|
||
<div class="pane" id="pane-capture">
|
||
<div class="sec" style="padding-bottom:6px;">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;">
|
||
<div class="sec-title" style="margin:0;">本地抓图记录</div>
|
||
<button class="btn btn-default btn-sm" onclick="loadCaptures()">↺ 刷新</button>
|
||
</div>
|
||
</div>
|
||
<div id="capture-gallery">
|
||
<div class="empty-tip"><span class="ei">📸</span>暂无抓图记录<br>点击摄像头条目旁的 📸 按钮开始抓图</div>
|
||
</div>
|
||
|
||
<!-- 定时任务 -->
|
||
<div class="sec" style="margin-top:4px;">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
||
<div class="sec-title" style="margin:0;">⏱ 定时抓图任务</div>
|
||
<button class="btn btn-default btn-sm" onclick="loadCaptureTasks()">↺ 刷新</button>
|
||
</div>
|
||
<!-- 新建任务 -->
|
||
<div class="field">
|
||
<div class="field-lbl">监控点编号</div>
|
||
<input class="inp" id="task-code" placeholder="cameraIndexCode">
|
||
</div>
|
||
<div class="row2">
|
||
<div class="field">
|
||
<div class="field-lbl">间隔(秒,≥10)</div>
|
||
<input class="inp" type="number" id="task-interval" value="60" min="10">
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-lbl">最大次数(0=无限)</div>
|
||
<input class="inp" type="number" id="task-max" value="0" min="0">
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" onclick="createTask()" style="margin-bottom:10px;">✚ 创建任务</button>
|
||
</div>
|
||
<div id="task-list" style="padding:0 8px 12px;">
|
||
<div class="empty-tip"><span class="ei">⏱</span>暂无定时任务</div>
|
||
</div>
|
||
</div><!-- /pane-capture -->
|
||
|
||
<!-- ══ 更多(高级控制) ══ -->
|
||
<div class="pane" id="pane-more">
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">旋转 & 缩放</div>
|
||
<div class="row2" style="margin-bottom:6px;">
|
||
<div>
|
||
<div class="field-lbl">旋转角度</div>
|
||
<select class="sel" id="sel-rotate" onchange="doRotate()">
|
||
<option value="0">0°</option>
|
||
<option value="90">90°</option>
|
||
<option value="180">180°</option>
|
||
<option value="270">270°</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<div class="field-lbl">显示比例</div>
|
||
<select class="sel" id="sel-scale" onchange="doScale()">
|
||
<option value="fill">平铺</option>
|
||
<option value="16:9">16:9</option>
|
||
<option value="4:3">4:3</option>
|
||
<option value="9:16">9:16</option>
|
||
<option value="3:4">3:4</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-default btn-sm" onclick="scaleCancel()">↩ 还原</button>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">智能信息 & 电子放大</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-default btn-sm" onclick="intellectTrigger(true)">开启智能</button>
|
||
<button class="btn btn-default btn-sm" onclick="intellectTrigger(false)">关闭智能</button>
|
||
<button class="btn btn-default btn-sm" onclick="enlarge()">开启放大</button>
|
||
<button class="btn btn-default btn-sm" onclick="enlargeClose()">关闭放大</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">视频信息</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-default btn-sm" onclick="getVideoInfo()">获取视频信息</button>
|
||
<button class="btn btn-default btn-sm" onclick="getOSDTime()">获取OSD时间</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">抓图水印</div>
|
||
<div class="row2">
|
||
<div class="field">
|
||
<div class="field-lbl">文字</div>
|
||
<input class="inp" id="wm-text" value="hikvision">
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-lbl">间隔(px)</div>
|
||
<input class="inp" type="number" id="wm-space" value="240">
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-lbl">角度(°)</div>
|
||
<input class="inp" type="number" id="wm-angle" value="-15">
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-lbl">颜色</div>
|
||
<input class="inp" id="wm-color" value="rgba(255,255,255,0.5)">
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-lbl">字体</div>
|
||
<input class="inp" id="wm-font" value="20px Microsoft YaHei">
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-default btn-sm" onclick="setWatermark()">设置水印</button>
|
||
<button class="btn btn-danger btn-sm" onclick="cancelWatermark()">清除水印</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec">
|
||
<div class="sec-title">录像 & 对讲录音</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-warn btn-sm" onclick="recordStart()">⏺ 录制MP4</button>
|
||
<button class="btn btn-danger btn-sm" onclick="recordStop()">⏹ 停止录制</button>
|
||
<button class="btn btn-warn btn-sm" onclick="recordStartTalk()">🎤 录制音频</button>
|
||
<button class="btn btn-danger btn-sm" onclick="recordStopTalk()">⏹ 停止录音</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="height:20px;"></div>
|
||
</div><!-- /pane-more -->
|
||
|
||
</div><!-- /panel -->
|
||
|
||
<!-- 状态栏 -->
|
||
<div class="statusbar">
|
||
<div class="sb-dot" id="sb-dot"></div>
|
||
<span class="sb-text" id="sb-text">就绪</span>
|
||
<span class="sb-rate" id="sb-rate"></span>
|
||
</div>
|
||
|
||
</div><!-- /root -->
|
||
|
||
<!-- 灯箱 -->
|
||
<div id="lightbox" onclick="closeLightbox()">
|
||
<span id="lb-close" onclick="closeLightbox()">✕</span>
|
||
<img id="lb-img" src="" alt="预览" onclick="event.stopPropagation()">
|
||
<div id="lb-info"></div>
|
||
</div>
|
||
|
||
<div id="toast"></div>
|
||
|
||
<!-- H5player 核心(与 playctrl* 一起在 static/ 根) -->
|
||
<script src="/h5player.min.js"></script>
|
||
<script>
|
||
/* ════════════════════════════════════════════════════════
|
||
全局状态
|
||
════════════════════════════════════════════════════════ */
|
||
var player = null;
|
||
var jsMode = 1; // 0=MSE, 1=高级解码
|
||
var isMuted = true;
|
||
var selCode = ''; // 当前选中摄像头编号
|
||
var selName = ''; // 当前选中摄像头名称
|
||
|
||
/* 区域树导航栈:[{code, name}, ...], code=null 表示根节点 */
|
||
var _regionStack = [];
|
||
/* 当前层级完整数据(用于搜索过滤) */
|
||
var _curRegions = [];
|
||
var _curChannels = [];
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
播放器初始化
|
||
════════════════════════════════════════════════════════ */
|
||
function initPlayer() {
|
||
player = new JSPlugin({
|
||
szId: 'player',
|
||
szBasePath: '/', // h5player.min.js + playctrl* 在 static/ 根
|
||
iMaxSplit: 4,
|
||
iCurrentSplit: 1,
|
||
openDebug: true,
|
||
mseWorkerEnable: false,
|
||
bSupporDoubleClickFull: true,
|
||
oStyle: { borderSelect: '#0ea5e9' }
|
||
});
|
||
|
||
player.JS_SetWindowControlCallback({
|
||
windowEventSelect: function(idx) { setStatus('已选中窗口 ' + idx, ''); },
|
||
pluginErrorHandler: function(idx, code, err) {
|
||
setStatus('错误[' + idx + ']: ' + (err || code), 'error');
|
||
toast('❌ ' + (err || code));
|
||
},
|
||
windowFullCcreenChange: function(f) { setStatus(f ? '全屏模式' : '退出全屏', ''); },
|
||
firstFrameDisplay: function(idx, w, h) {
|
||
setStatus('播放中 ' + w + '×' + h, 'playing');
|
||
setConnDot(true);
|
||
},
|
||
StreamEnd: function(idx) { setStatus('流已结束[' + idx + ']', ''); setConnDot(false); },
|
||
performanceLack: function() { toast('⚠ 性能不足,建议降低分辨率'); }
|
||
});
|
||
|
||
window.addEventListener('resize', function() {
|
||
if (player) player.JS_Resize();
|
||
adjustPlayerHeight();
|
||
});
|
||
adjustPlayerHeight();
|
||
|
||
/* 默认回放时间:最近1小时 */
|
||
var now = new Date();
|
||
var ago = new Date(now - 3600000);
|
||
document.getElementById('pb-end').value = fmtDtLocal(now);
|
||
document.getElementById('pb-start').value = fmtDtLocal(ago);
|
||
document.getElementById('pb-seek').value = fmtDtLocal(ago);
|
||
}
|
||
|
||
function adjustPlayerHeight() {
|
||
var w = document.getElementById('player').offsetWidth;
|
||
var h = Math.min(Math.round(w * 9 / 16), Math.round(window.innerHeight * 0.36));
|
||
document.getElementById('player').style.height = h + 'px';
|
||
if (player) player.JS_Resize();
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
UI 工具
|
||
════════════════════════════════════════════════════════ */
|
||
function setStatus(text, state) {
|
||
document.getElementById('sb-dot').className = 'sb-dot' + (state ? ' ' + state : '');
|
||
document.getElementById('sb-text').textContent = text;
|
||
}
|
||
function setRateBadge(r) {
|
||
document.getElementById('sb-rate').textContent = (r && r !== 1) ? r + '×' : '';
|
||
}
|
||
function setConnDot(on) {
|
||
document.getElementById('conn-dot').className = 'conn-dot' + (on ? ' on' : '');
|
||
document.getElementById('conn-label').textContent = on ? '已连接' : '未连接';
|
||
}
|
||
var _tid;
|
||
function toast(msg, ms) {
|
||
var el = document.getElementById('toast');
|
||
el.textContent = msg; el.className = 'show';
|
||
clearTimeout(_tid);
|
||
_tid = setTimeout(function(){ el.className = ''; }, ms || 2500);
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
标签页切换
|
||
════════════════════════════════════════════════════════ */
|
||
function switchTab(name) {
|
||
['region','preview','playback','capture','more'].forEach(function(n) {
|
||
document.getElementById('tab-' + n).classList.toggle('active', n === name);
|
||
document.getElementById('pane-' + n).classList.toggle('active', n === name);
|
||
});
|
||
if (name === 'capture') { loadCaptures(); loadCaptureTasks(); }
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
分屏 / 模式
|
||
════════════════════════════════════════════════════════ */
|
||
function setSplit(n) {
|
||
[1,2,3,4].forEach(function(x){
|
||
document.getElementById('sp'+x).classList.toggle('active', x === n);
|
||
});
|
||
if (!player) return;
|
||
player.JS_ArrangeWindow(n)
|
||
.then(function(){ setStatus('分屏: ' + n + '×' + n, ''); })
|
||
.catch(function(e){ toast('分屏失败: ' + e); });
|
||
}
|
||
function cycleMode() {
|
||
jsMode = jsMode === 0 ? 1 : 0;
|
||
var btn = document.getElementById('mode-btn');
|
||
btn.textContent = jsMode === 0 ? 'MSE' : 'HDC';
|
||
btn.classList.toggle('hi', jsMode === 1);
|
||
toast(jsMode === 0 ? '普通模式(MSE)' : '高级解码模式');
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
全屏
|
||
════════════════════════════════════════════════════════ */
|
||
function wholeFullScreen() {
|
||
if (!player) return;
|
||
player.JS_FullScreenDisplay(true).catch(function(e){ toast('全屏失败: ' + e); });
|
||
}
|
||
function singleFullScreen() {
|
||
if (!player) return;
|
||
player.JS_FullScreenSingle(player.currentWindowIndex).catch(function(e){ toast(e); });
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
★ 区域树导航(与 index.html 完全对齐的 API)
|
||
GET /api/regions?cascadeFlag=0&pageSize=200[&parentCode={code}]
|
||
GET /api/regions/{code}/channels?cascadeFlag=0&pageSize=200
|
||
════════════════════════════════════════════════════════ */
|
||
function loadRegionTree() {
|
||
_regionStack = [];
|
||
_loadRegionLevel(null);
|
||
}
|
||
|
||
function _loadRegionLevel(parentCode) {
|
||
var listEl = document.getElementById('region-list');
|
||
listEl.innerHTML = '<div class="empty-tip"><div class="spin"></div><br>加载中…</div>';
|
||
_renderBreadcrumb();
|
||
|
||
var regionUrl = '/api/regions?cascadeFlag=0&pageSize=200'
|
||
+ (parentCode ? '&parentCode=' + encodeURIComponent(parentCode) : '');
|
||
var channelUrl = parentCode
|
||
? '/api/regions/' + encodeURIComponent(parentCode) + '/channels?cascadeFlag=0&pageSize=200'
|
||
: null;
|
||
|
||
var regionP = fetch(regionUrl).then(function(r){ return r.json(); });
|
||
var channelP = channelUrl
|
||
? fetch(channelUrl).then(function(r){ return r.json(); })
|
||
: Promise.resolve(null);
|
||
|
||
Promise.all([regionP, channelP]).then(function(results) {
|
||
var rRes = results[0];
|
||
var cRes = results[1];
|
||
_curRegions = (rRes.code === 200 && rRes.data && rRes.data.list) ? rRes.data.list : [];
|
||
_curChannels = (cRes && cRes.code === 200 && cRes.data && cRes.data.list) ? cRes.data.list : [];
|
||
_renderRegionLevel(_curRegions, _curChannels);
|
||
}).catch(function() {
|
||
listEl.innerHTML = '<div class="empty-tip"><span class="ei">⚠</span>加载失败,请确认后端服务已启动</div>';
|
||
});
|
||
}
|
||
|
||
function _renderRegionLevel(regions, channels) {
|
||
var el = document.getElementById('region-list');
|
||
var html = '';
|
||
|
||
/* 返回上级 */
|
||
if (_regionStack.length > 0) {
|
||
html += '<div class="back-row" onclick="_goBack()">◀ 返回上级</div>';
|
||
}
|
||
|
||
/* 子区域 */
|
||
if (regions.length > 0) {
|
||
html += '<div class="group-title">区域 <span class="group-count">' + regions.length + '</span></div>';
|
||
regions.forEach(function(r) {
|
||
var code = escHtml(r.regionIndexCode || '');
|
||
var name = escHtml(r.regionName || '未命名');
|
||
html += '<div class="region-item" onclick="_drillDown(\'' + code + '\',\'' + name + '\')">'
|
||
+ '<span class="region-icon">📁</span>'
|
||
+ '<span class="region-name">' + name + '</span>'
|
||
+ '<span class="region-arrow">▶</span>'
|
||
+ '</div>';
|
||
});
|
||
}
|
||
|
||
/* 摄像头通道 */
|
||
if (channels.length > 0) {
|
||
html += '<div class="group-title" style="margin-top:4px;">摄像头 <span class="group-count">' + channels.length + '</span></div>';
|
||
channels.forEach(function(cam) {
|
||
html += _buildCamItem(cam);
|
||
});
|
||
}
|
||
|
||
if (regions.length === 0 && channels.length === 0) {
|
||
html += '<div class="empty-tip"><span class="ei">📭</span>该区域暂无子区域或摄像头</div>';
|
||
}
|
||
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
function _buildCamItem(cam) {
|
||
var code = cam.cameraIndexCode || '';
|
||
var name = cam.cameraName || '未命名';
|
||
var st = cam.onlineStatus;
|
||
var icon = (st == null || st === '') ? '📷' : (st === '1' ? '🟢' : '🔴');
|
||
var badge = (st == null || st === '') ? 'b-unk' : (st === '1' ? 'b-on' : 'b-off');
|
||
var btxt = (st == null || st === '') ? '未知' : (st === '1' ? '在线' : '离线');
|
||
var ec = escHtml(code), en = escHtml(name);
|
||
return '<div class="cam-item" id="ci-' + ec + '" onclick="selectCam(\'' + ec + '\',\'' + en + '\')">'
|
||
+ '<span class="cam-icon">' + icon + '</span>'
|
||
+ '<div class="cam-info">'
|
||
+ '<div class="cam-name">' + en + '</div>'
|
||
+ '<div class="cam-code">' + ec + '</div>'
|
||
+ '</div>'
|
||
+ '<span class="cam-badge ' + badge + '">' + btxt + '</span>'
|
||
+ '<div class="cam-actions">'
|
||
+ '<button class="ca-btn play" title="立即预览" onclick="event.stopPropagation();quickPlay(\'' + ec + '\',\'' + en + '\')">▶</button>'
|
||
+ '<button class="ca-btn cap" title="API抓图" onclick="event.stopPropagation();captureImage(\'' + ec + '\',\'' + en + '\',this)">📸</button>'
|
||
+ '</div>'
|
||
+ '</div>';
|
||
}
|
||
|
||
function _drillDown(code, name) {
|
||
_regionStack.push({ code: code, name: name });
|
||
_loadRegionLevel(code);
|
||
}
|
||
function _goBack() {
|
||
_regionStack.pop();
|
||
var parent = _regionStack.length > 0 ? _regionStack[_regionStack.length - 1] : { code: null };
|
||
_loadRegionLevel(parent.code);
|
||
}
|
||
function _jumpTo(idx) {
|
||
if (idx < 0) { _regionStack = []; _loadRegionLevel(null); return; }
|
||
_regionStack = _regionStack.slice(0, idx + 1);
|
||
_loadRegionLevel(_regionStack[idx].code);
|
||
}
|
||
|
||
function _renderBreadcrumb() {
|
||
var el = document.getElementById('breadcrumb');
|
||
if (_regionStack.length === 0) {
|
||
el.innerHTML = '<span class="bc-cur">根节点</span>'; return;
|
||
}
|
||
var html = '<span class="bc-item" onclick="_jumpTo(-1)">根节点</span>';
|
||
_regionStack.forEach(function(item, i) {
|
||
html += '<span class="bc-sep"> / </span>';
|
||
if (i === _regionStack.length - 1) {
|
||
html += '<span class="bc-cur">' + escHtml(item.name) + '</span>';
|
||
} else {
|
||
html += '<span class="bc-item" onclick="_jumpTo(' + i + ')">' + escHtml(item.name) + '</span>';
|
||
}
|
||
});
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
/* 搜索过滤(仅过滤当前层摄像头,区域保持显示) */
|
||
function filterInCurrentView(kw) {
|
||
if (!kw) { _renderRegionLevel(_curRegions, _curChannels); return; }
|
||
var lk = kw.toLowerCase();
|
||
var filtered = _curChannels.filter(function(c) {
|
||
return (c.cameraName || '').toLowerCase().indexOf(lk) >= 0
|
||
|| (c.cameraIndexCode || '').toLowerCase().indexOf(lk) >= 0;
|
||
});
|
||
_renderRegionLevel(_curRegions, filtered);
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
选中摄像头
|
||
════════════════════════════════════════════════════════ */
|
||
function selectCam(code, name) {
|
||
selCode = code; selName = name;
|
||
/* 同步到预览/回放面板 */
|
||
document.getElementById('cur-code').value = code;
|
||
document.getElementById('pb-code').value = code;
|
||
document.getElementById('task-code').value = code;
|
||
/* 高亮 */
|
||
document.querySelectorAll('.cam-item').forEach(function(el){ el.classList.remove('active'); });
|
||
var el = document.getElementById('ci-' + code);
|
||
if (el) el.classList.add('active');
|
||
setStatus('已选中: ' + name, '');
|
||
toast('已选中: ' + name);
|
||
}
|
||
|
||
/* 一键预览 */
|
||
function quickPlay(code, name) {
|
||
selectCam(code, name);
|
||
switchTab('preview');
|
||
startPreview();
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
★ 预览(调后端 v2 接口获取播放地址)
|
||
GET /api/cameras/{code}/preview-url/v2?protocol=ws|wss|rtsp|hls|flv&streamType=&transmode=
|
||
════════════════════════════════════════════════════════ */
|
||
function startPreview() {
|
||
if (!player) { toast('播放器未就绪'); return; }
|
||
var code = document.getElementById('cur-code').value.trim();
|
||
var sType = document.getElementById('stream-type').value;
|
||
var tMode = document.getElementById('trans-mode').value;
|
||
var proto = document.getElementById('stream-proto').value;
|
||
if (!code) { toast('⚠ 请先在区域列表中选择摄像头'); return; }
|
||
|
||
setStatus('获取预览地址…', '');
|
||
toast('⏳ 连接中…', 6000);
|
||
|
||
fetch('/api/cameras/' + encodeURIComponent(code)
|
||
+ '/preview-url/v2?protocol=' + proto + '&streamType=' + sType + '&transmode=' + tMode)
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res) {
|
||
if (res.code !== 200 || !res.data || !res.data.url) {
|
||
setStatus('获取地址失败', 'error');
|
||
toast('❌ 获取预览地址失败: ' + (res.msg || JSON.stringify(res)));
|
||
return;
|
||
}
|
||
var wss = res.data.url;
|
||
document.getElementById('wss-url').value = wss;
|
||
|
||
player.JS_Play(wss, { playURL: wss, mode: jsMode, keepDecoder: 0 }, player.currentWindowIndex)
|
||
.then(function(){
|
||
setStatus('预览: ' + (selName || code), 'playing');
|
||
setConnDot(true); setRateBadge(1);
|
||
toast('▶ 预览已开始');
|
||
})
|
||
.catch(function(e){ setStatus('播放失败: ' + e, 'error'); toast('❌ ' + e); });
|
||
})
|
||
.catch(function(e){ setStatus('网络错误', 'error'); toast('❌ ' + e.message); });
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
★ 回放(调后端接口获取播放地址)
|
||
GET /api/cameras/{code}/playback-url?beginTime=&endTime=&protocol=ws|wss|rtsp&transmode=1&recordLocation=0
|
||
════════════════════════════════════════════════════════ */
|
||
function playbackStart() {
|
||
_doPlayback(false);
|
||
}
|
||
function reversekStart() {
|
||
_doPlayback(true);
|
||
}
|
||
function _doPlayback(reverse) {
|
||
if (!player) { toast('播放器未就绪'); return; }
|
||
var code = document.getElementById('pb-code').value.trim();
|
||
var start = document.getElementById('pb-start').value;
|
||
var end = document.getElementById('pb-end').value;
|
||
/* 回放协议跟随预览协议选择 */
|
||
var proto = document.getElementById('stream-proto') ? document.getElementById('stream-proto').value : 'ws';
|
||
if (!code) { toast('⚠ 请先选择摄像头'); return; }
|
||
if (!start || !end) { toast('⚠ 请选择时间段'); return; }
|
||
|
||
var startIso = toIso8601(start);
|
||
var endIso = toIso8601(end);
|
||
|
||
setStatus('获取回放地址…', '');
|
||
toast('⏳ 获取中…', 8000);
|
||
|
||
fetch('/api/cameras/' + encodeURIComponent(code) + '/playback-url'
|
||
+ '?beginTime=' + encodeURIComponent(startIso)
|
||
+ '&endTime=' + encodeURIComponent(endIso)
|
||
+ '&protocol=' + proto + '&transmode=1&recordLocation=0')
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res) {
|
||
if (res.code !== 200 || !res.data || !res.data.url) {
|
||
setStatus('获取回放地址失败', 'error');
|
||
toast('❌ ' + (res.msg || JSON.stringify(res)));
|
||
return;
|
||
}
|
||
var wss = res.data.url;
|
||
document.getElementById('pb-wss-url').value = wss;
|
||
|
||
var opt = { playURL: wss, mode: jsMode, keepDecoder: 0 };
|
||
if (reverse) opt.PlayBackMode = 3;
|
||
|
||
player.JS_Play(wss, opt, player.currentWindowIndex, startIso, endIso)
|
||
.then(function(){
|
||
setStatus((reverse ? '倒放' : '回放') + ': ' + code, 'playing');
|
||
setConnDot(true); setRateBadge(reverse ? -1 : 1);
|
||
toast((reverse ? '◀ 倒放' : '▶ 回放') + '已开始');
|
||
})
|
||
.catch(function(e){ setStatus('回放失败: ' + e, 'error'); toast('❌ ' + e); });
|
||
})
|
||
.catch(function(e){ setStatus('网络错误', 'error'); toast('❌ ' + e.message); });
|
||
}
|
||
|
||
/* 停止 / 暂停 / 恢复 / Seek */
|
||
function stopPlay() {
|
||
if (!player) return;
|
||
player.JS_Stop()
|
||
.then(function(){ setStatus('已停止', ''); setConnDot(false); setRateBadge(0); })
|
||
.catch(function(e){ toast(e); });
|
||
}
|
||
function stopAllPlay() {
|
||
if (!player) return;
|
||
player.JS_StopRealPlayAll()
|
||
.then(function(){ setStatus('全部已停止', ''); setConnDot(false); setRateBadge(0); })
|
||
.catch(function(e){ toast(e); });
|
||
}
|
||
function playbackPause() {
|
||
if (!player) return;
|
||
player.JS_Pause(player.currentWindowIndex)
|
||
.then(function(){ setStatus('已暂停', ''); }).catch(function(e){ toast(e); });
|
||
}
|
||
function playbackResume(fwd) {
|
||
if (!player) return;
|
||
player.JS_Resume(player.currentWindowIndex, fwd)
|
||
.then(function(){ setStatus(fwd ? '正向恢复' : '倒向恢复', 'playing'); }).catch(function(e){ toast(e); });
|
||
}
|
||
function seekTo() {
|
||
if (!player) return;
|
||
var seek = toIso8601(document.getElementById('pb-seek').value);
|
||
var end = toIso8601(document.getElementById('pb-end').value);
|
||
player.JS_Seek(player.currentWindowIndex, seek, end)
|
||
.then(function(){ setStatus('已跳转', 'playing'); toast('📍 跳转成功'); })
|
||
.catch(function(e){ toast('跳转失败: ' + e); });
|
||
}
|
||
function playbackSlow() {
|
||
if (!player) return;
|
||
player.JS_Slow()
|
||
.then(function(r){ setRateBadge(r); setStatus('慢放 ' + r + '×', 'playing'); })
|
||
.catch(function(e){ toast(e); });
|
||
}
|
||
function playbackFast() {
|
||
if (!player) return;
|
||
player.JS_Fast()
|
||
.then(function(r){ setRateBadge(r); setStatus('快放 ' + r + '×', 'playing'); })
|
||
.catch(function(e){ toast(e); });
|
||
}
|
||
function frameForward() {
|
||
if (!player) return;
|
||
player.JS_FrameForward(player.currentWindowIndex)
|
||
.then(function(){ setStatus('单帧进', ''); }).catch(function(e){ toast(e); });
|
||
}
|
||
function frameBack() {
|
||
if (!player) return;
|
||
player.JS_FrameBack(player.currentWindowIndex)
|
||
.then(function(){ setStatus('单帧退', ''); }).catch(function(e){ toast(e); });
|
||
}
|
||
function changeMode() {
|
||
if (!player) return;
|
||
player.JS_ChangeMode(player.currentWindowIndex);
|
||
toast('正倒放已切换');
|
||
}
|
||
function setRate(r) {
|
||
document.querySelectorAll('.rate-pill').forEach(function(p){
|
||
p.classList.toggle('active', parseFloat(p.dataset.r) === r);
|
||
});
|
||
if (!player) return;
|
||
player.JS_Speed(player.currentWindowIndex, r)
|
||
.then(function(rate){ setRateBadge(rate); setStatus('速率 ' + rate + '×', 'playing'); })
|
||
.catch(function(e){ toast(e); });
|
||
}
|
||
|
||
/* 对讲 */
|
||
function talkStart() {
|
||
var url = document.getElementById('wss-url').value.trim();
|
||
if (!url || !player) { toast('⚠ 请先预览后再对讲'); return; }
|
||
player.JS_StartTalk(url)
|
||
.then(function(){ toast('🎙 对讲已开始'); setStatus('对讲中…', 'playing'); })
|
||
.catch(function(e){ toast('对讲失败: ' + e); });
|
||
}
|
||
function talkStop() {
|
||
if (!player) return;
|
||
player.JS_StopTalk()
|
||
.then(function(){ setStatus('对讲已停止', ''); }).catch(function(e){ toast(e); });
|
||
}
|
||
|
||
/* 声音 */
|
||
function openSound() {
|
||
if (!player) return;
|
||
player.JS_OpenSound()
|
||
.then(function(){ isMuted = false; document.getElementById('vol-icon').textContent = '🔊'; })
|
||
.catch(function(e){ toast(e); });
|
||
}
|
||
function closeSound() {
|
||
if (!player) return;
|
||
player.JS_CloseSound()
|
||
.then(function(){ isMuted = true; document.getElementById('vol-icon').textContent = '🔇'; })
|
||
.catch(function(e){ toast(e); });
|
||
}
|
||
function toggleMute() { isMuted ? openSound() : closeSound(); }
|
||
function setVolume(val) {
|
||
document.getElementById('vol-val').textContent = val;
|
||
if (!player) return;
|
||
player.JS_SetVolume(player.currentWindowIndex, parseInt(val)).catch(function(){});
|
||
}
|
||
|
||
/* 录像 / 对讲录音 */
|
||
function recordStart() {
|
||
if (!player) return;
|
||
var fn = _ts() + '.mp4';
|
||
player.JS_StartSaveEx(player.currentWindowIndex, fn, 5, { irecordType: 1 })
|
||
.then(function(){ toast('⏺ 录制: ' + fn); setStatus('录制中', 'playing'); })
|
||
.catch(function(e){ toast('录制失败: ' + e); });
|
||
}
|
||
function recordStop() {
|
||
if (!player) return;
|
||
player.JS_StopSave(player.currentWindowIndex)
|
||
.then(function(){ toast('⏹ 录制已停止'); setStatus('保存中…', ''); })
|
||
.catch(function(e){ toast(e); });
|
||
}
|
||
function recordStartTalk() {
|
||
if (!player) return;
|
||
player.JS_StartSaveTalk(_ts() + '.mp3', 2)
|
||
.then(function(){ toast('🎤 音频录制中'); }).catch(function(e){ toast(e); });
|
||
}
|
||
function recordStopTalk() {
|
||
if (!player) return;
|
||
player.JS_StopSaveTalk()
|
||
.then(function(){ toast('⏹ 录音已停止'); }).catch(function(e){ toast(e); });
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
★ 后端抓图(与 index.html 完全对齐)
|
||
POST /api/captures/{cameraIndexCode}?cameraName={name}
|
||
════════════════════════════════════════════════════════ */
|
||
function captureImage(code, name, btn) {
|
||
if (!code) { toast('⚠ 请先选择摄像头'); return; }
|
||
if (btn) { btn.textContent = '⏳'; btn.disabled = true; }
|
||
toast('📸 抓图中…', 5000);
|
||
setStatus('抓图: ' + (name || code) + '…', '');
|
||
|
||
fetch('/api/captures/' + encodeURIComponent(code)
|
||
+ '?cameraName=' + encodeURIComponent(name || ''), { method: 'POST' })
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res) {
|
||
if (btn) { btn.textContent = '📸'; btn.disabled = false; }
|
||
if (res.code === 200 && res.data) {
|
||
toast('✅ 抓图成功: ' + res.data.filename, 4000);
|
||
setStatus('抓图完成: ' + res.data.filename, '');
|
||
switchTab('capture');
|
||
} else {
|
||
toast('❌ 抓图失败: ' + (res.msg || '未知'), 5000);
|
||
setStatus('抓图失败', 'error');
|
||
}
|
||
})
|
||
.catch(function(e) {
|
||
if (btn) { btn.textContent = '📸'; btn.disabled = false; }
|
||
toast('❌ 请求失败: ' + e.message, 5000);
|
||
});
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
★ 抓图图库(与 index.html 完全对齐)
|
||
GET /api/captures
|
||
════════════════════════════════════════════════════════ */
|
||
function loadCaptures() {
|
||
var el = document.getElementById('capture-gallery');
|
||
el.innerHTML = '<div class="empty-tip"><div class="spin"></div><br>加载中…</div>';
|
||
|
||
fetch('/api/captures')
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res) {
|
||
if (res.code !== 200 || !res.data || !res.data.length) {
|
||
el.innerHTML = '<div class="empty-tip"><span class="ei">📸</span>暂无抓图记录</div>';
|
||
return;
|
||
}
|
||
var html = '<div class="gallery">';
|
||
res.data.forEach(function(item) {
|
||
var fn = escHtml(item.filename || '');
|
||
var url = escHtml(item.localUrl || '');
|
||
var t = escHtml(item.captureTime || '');
|
||
var code = escHtml(item.cameraIndexCode || '');
|
||
var kb = item.fileSizeBytes ? Math.round(item.fileSizeBytes / 1024) + ' KB' : '';
|
||
html += '<div class="g-card">'
|
||
+ '<img src="' + url + '" alt="' + fn + '" loading="lazy"'
|
||
+ ' onclick="openLightbox(\'' + url + '\',\'' + fn + '\',\'' + t + '\')"'
|
||
+ ' onerror="this.style.background=\'#1e293b\'">'
|
||
+ '<div class="g-info">'
|
||
+ '<div class="g-name" title="' + code + '">' + (code || fn) + '</div>'
|
||
+ '<div class="g-time">' + t + (kb ? ' · ' + kb : '') + '</div>'
|
||
+ '</div>'
|
||
+ '<div class="g-actions">'
|
||
+ '<a class="g-act" href="' + url + '" download="' + fn + '">⬇ 下载</a>'
|
||
+ '<span class="g-act del" onclick="deleteCapture(\'' + fn + '\',this)">🗑 删除</span>'
|
||
+ '</div>'
|
||
+ '</div>';
|
||
});
|
||
html += '</div>';
|
||
el.innerHTML = html;
|
||
})
|
||
.catch(function() {
|
||
el.innerHTML = '<div class="empty-tip"><span class="ei">⚠</span>加载失败</div>';
|
||
});
|
||
}
|
||
|
||
function deleteCapture(filename, btn) {
|
||
if (!confirm('确认删除 ' + filename + '?')) return;
|
||
fetch('/api/captures/' + encodeURIComponent(filename), { method: 'DELETE' })
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res) {
|
||
if (res.code === 200) { toast('🗑 已删除'); loadCaptures(); }
|
||
else { toast('❌ 删除失败: ' + (res.msg || ''), 4000); }
|
||
})
|
||
.catch(function(){ toast('❌ 请求失败', 3000); });
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
★ 定时任务(与 index.html 完全对齐)
|
||
════════════════════════════════════════════════════════ */
|
||
function createTask() {
|
||
var code = document.getElementById('task-code').value.trim();
|
||
var interval = parseInt(document.getElementById('task-interval').value) || 60;
|
||
var maxCap = parseInt(document.getElementById('task-max').value) || 0;
|
||
if (!code) { toast('⚠ 请输入监控点编号'); return; }
|
||
if (interval < 10) { toast('⚠ 间隔最小10秒'); return; }
|
||
|
||
fetch('/api/capture-tasks', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ cameraIndexCode: code, cameraName: selName || code, intervalSeconds: interval, maxCaptures: maxCap })
|
||
})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res) {
|
||
if (res.code === 200) {
|
||
toast('✅ 任务已创建', 3000);
|
||
document.getElementById('task-code').value = '';
|
||
loadCaptureTasks();
|
||
} else { toast('❌ ' + (res.msg || ''), 4000); }
|
||
})
|
||
.catch(function(e){ toast('❌ ' + e.message, 4000); });
|
||
}
|
||
|
||
function loadCaptureTasks() {
|
||
var el = document.getElementById('task-list');
|
||
el.innerHTML = '<div class="empty-tip"><div class="spin"></div><br>加载中…</div>';
|
||
|
||
fetch('/api/capture-tasks')
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res) {
|
||
if (res.code !== 200 || !res.data || !res.data.length) {
|
||
el.innerHTML = '<div class="empty-tip"><span class="ei">⏱</span>暂无定时任务</div>';
|
||
return;
|
||
}
|
||
var html = '';
|
||
res.data.forEach(function(t) {
|
||
var running = t.status === 'RUNNING', err = t.status === 'ERROR';
|
||
var cls = running ? 'task-card running' : (err ? 'task-card error' : 'task-card');
|
||
var stTxt = running ? '▶ 运行中' : (err ? '⚠ 出错' : '⏹ 已停止');
|
||
var maxTxt = t.maxCaptures > 0 ? '/' + t.maxCaptures : '';
|
||
html += '<div class="' + cls + '">'
|
||
+ '<div class="task-hd">'
|
||
+ '<span class="task-name">' + escHtml(t.cameraName || t.cameraIndexCode) + '</span>'
|
||
+ '<span class="task-st ' + (t.status || 'STOPPED') + '">' + stTxt + '</span>'
|
||
+ '</div>'
|
||
+ '<div class="task-meta">'
|
||
+ '📷 ' + escHtml(t.cameraIndexCode) + '<br>'
|
||
+ '⏱ 每 ' + t.intervalSeconds + ' 秒 · 📸 已抓 ' + (t.captureCount || 0) + maxTxt + ' 次';
|
||
if (t.lastCaptureTime) html += '<br>🕐 最近: ' + escHtml(t.lastCaptureTime);
|
||
if (t.lastError) html += '<br><span style="color:var(--yellow)">⚠ ' + escHtml(t.lastError.substring(0,50)) + '</span>';
|
||
html += '</div>';
|
||
if (t.lastCaptureUrl) {
|
||
html += '<img class="task-thumb" src="' + escHtml(t.lastCaptureUrl) + '"'
|
||
+ ' onclick="openLightbox(\'' + escHtml(t.lastCaptureUrl) + '\',\'最近抓图\',\'\')"'
|
||
+ ' onerror="this.style.display=\'none\'">';
|
||
}
|
||
html += '<div class="task-btns">';
|
||
if (running || err) {
|
||
html += '<button class="t-btn stop" onclick="taskAction(\'' + t.id + '\',\'stop\')">⏸ 暂停</button>';
|
||
} else {
|
||
html += '<button class="t-btn start" onclick="taskAction(\'' + t.id + '\',\'start\')">▶ 启动</button>';
|
||
}
|
||
html += '<button class="t-btn" onclick="captureImage(\'' + escHtml(t.cameraIndexCode) + '\',\'' + escHtml(t.cameraName||'') + '\',null)">📸 立即抓图</button>';
|
||
html += '<button class="t-btn del" onclick="deleteTask(\'' + t.id + '\')">🗑</button>';
|
||
html += '</div></div>';
|
||
});
|
||
el.innerHTML = html;
|
||
})
|
||
.catch(function(){ el.innerHTML = '<div class="empty-tip"><span class="ei">⚠</span>加载失败</div>'; });
|
||
}
|
||
|
||
function taskAction(id, action) {
|
||
fetch('/api/capture-tasks/' + id + '/' + action, { method: 'PUT' })
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res) {
|
||
if (res.code === 200) { toast(action === 'start' ? '▶ 任务已启动' : '⏸ 任务已暂停'); loadCaptureTasks(); }
|
||
else { toast('❌ ' + (res.msg || ''), 4000); }
|
||
})
|
||
.catch(function(){ toast('❌ 请求失败', 3000); });
|
||
}
|
||
|
||
function deleteTask(id) {
|
||
if (!confirm('确认删除该任务?')) return;
|
||
fetch('/api/capture-tasks/' + id, { method: 'DELETE' })
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res) {
|
||
if (res.code === 200) { toast('🗑 已删除'); loadCaptureTasks(); }
|
||
else { toast('❌ ' + (res.msg || ''), 4000); }
|
||
})
|
||
.catch(function(){ toast('❌ 请求失败', 3000); });
|
||
}
|
||
|
||
/* 定时自动刷新任务(抓图Tab可见时每10秒刷新) */
|
||
setInterval(function() {
|
||
var el = document.getElementById('task-list');
|
||
var pane = document.getElementById('pane-capture');
|
||
if (el && pane && pane.classList.contains('active')) loadCaptureTasks();
|
||
}, 10000);
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
高级控制
|
||
════════════════════════════════════════════════════════ */
|
||
function doRotate() {
|
||
if (!player) return;
|
||
player.JS_Rotate(player.currentWindowIndex, parseInt(document.getElementById('sel-rotate').value))
|
||
.then(function(){ setStatus('旋转完成', ''); }).catch(function(e){ toast(e); });
|
||
}
|
||
function doScale() {
|
||
if (!player) return;
|
||
player.JS_Scale(player.currentWindowIndex, document.getElementById('sel-scale').value)
|
||
.then(function(){ setStatus('缩放完成', ''); }).catch(function(e){ toast(e); });
|
||
}
|
||
function scaleCancel() {
|
||
if (!player) return;
|
||
player.JS_ScaleCancel(player.currentWindowIndex).then(function(){
|
||
document.getElementById('sel-rotate').value = '0';
|
||
document.getElementById('sel-scale').value = 'fill';
|
||
setStatus('已还原', '');
|
||
});
|
||
}
|
||
function enlarge() {
|
||
if (!player) return;
|
||
player.JS_EnableZoom(player.currentWindowIndex)
|
||
.then(function(){ setStatus('电子放大:请框选区域', ''); }).catch(function(e){ toast(e); });
|
||
}
|
||
function enlargeClose() {
|
||
if (!player) return;
|
||
player.JS_DisableZoom(player.currentWindowIndex)
|
||
.then(function(){ setStatus('电子放大已关闭', ''); }).catch(function(e){ toast(e); });
|
||
}
|
||
function intellectTrigger(flag) {
|
||
if (!player) return;
|
||
player.JS_RenderALLPrivateData(player.currentWindowIndex, flag)
|
||
.then(function(){ setStatus('智能信息: ' + (flag ? '开启' : '关闭'), ''); }).catch(function(e){ toast(e); });
|
||
}
|
||
function getVideoInfo() {
|
||
if (!player) return;
|
||
player.JS_GetVideoInfo(player.currentWindowIndex)
|
||
.then(function(info){ toast(JSON.stringify(info)); setStatus(JSON.stringify(info), ''); }).catch(function(e){ toast(e); });
|
||
}
|
||
function getOSDTime() {
|
||
if (!player) return;
|
||
player.JS_GetOSDTime(player.currentWindowIndex)
|
||
.then(function(t){ toast('OSD: ' + new Date(t).toLocaleString()); }).catch(function(e){ toast(e); });
|
||
}
|
||
function setWatermark() {
|
||
if (!player) return;
|
||
player.JS_SetWatermarkConfig({
|
||
text: document.getElementById('wm-text').value,
|
||
space: parseInt(document.getElementById('wm-space').value) || 240,
|
||
rotateDegree: parseInt(document.getElementById('wm-angle').value) || -15,
|
||
font: document.getElementById('wm-font').value,
|
||
color: document.getElementById('wm-color').value
|
||
}).then(function(){ toast('水印已设置'); }).catch(function(e){ toast(e); });
|
||
}
|
||
function cancelWatermark() {
|
||
if (!player) return;
|
||
player.JS_CancelWatermarkConfig(); toast('水印已清除');
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
灯箱
|
||
════════════════════════════════════════════════════════ */
|
||
function openLightbox(url, name, time) {
|
||
document.getElementById('lb-img').src = url;
|
||
document.getElementById('lb-info').textContent = name + (time ? ' · ' + time : '');
|
||
document.getElementById('lightbox').className = 'show';
|
||
}
|
||
function closeLightbox() {
|
||
document.getElementById('lightbox').className = '';
|
||
document.getElementById('lb-img').src = '';
|
||
}
|
||
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeLightbox(); });
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
工具函数
|
||
════════════════════════════════════════════════════════ */
|
||
function pad2(n){ return n < 10 ? '0' + n : '' + n; }
|
||
function _ts() {
|
||
var d = new Date();
|
||
return d.getFullYear() + pad2(d.getMonth()+1) + pad2(d.getDate())
|
||
+ pad2(d.getHours()) + pad2(d.getMinutes()) + pad2(d.getSeconds());
|
||
}
|
||
function fmtDtLocal(d) {
|
||
return d.getFullYear() + '-' + pad2(d.getMonth()+1) + '-' + pad2(d.getDate())
|
||
+ 'T' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
|
||
}
|
||
function toIso8601(dtlocalVal) {
|
||
if (!dtlocalVal) return '';
|
||
var d = new Date(dtlocalVal);
|
||
var offset = -d.getTimezoneOffset();
|
||
var sign = offset >= 0 ? '+' : '-';
|
||
var oh = pad2(Math.floor(Math.abs(offset) / 60));
|
||
var om = pad2(Math.abs(offset) % 60);
|
||
return d.getFullYear() + '-' + pad2(d.getMonth()+1) + '-' + pad2(d.getDate())
|
||
+ 'T' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds())
|
||
+ '.' + ('00' + d.getMilliseconds()).slice(-3) + sign + oh + ':' + om;
|
||
}
|
||
function escHtml(s) {
|
||
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||
}
|
||
function copyText(id) {
|
||
var v = document.getElementById(id).value;
|
||
if (!v) { toast('⚠ 无内容可复制'); return; }
|
||
if (navigator.clipboard) { navigator.clipboard.writeText(v).then(function(){ toast('📋 已复制'); }); }
|
||
else { toast('⚠ 当前环境不支持复制'); }
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
启动
|
||
════════════════════════════════════════════════════════ */
|
||
window.addEventListener('DOMContentLoaded', function() {
|
||
initPlayer();
|
||
loadRegionTree(); // 启动时自动加载区域树
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|