safesight-control/internal/web/ui/templates/assets.html

411 lines
22 KiB
HTML
Raw 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.

{{define "asset_tabs"}}
<div class="card-tabs asset-tab-wrap">
<ul class="nav nav-tabs asset-tabs" role="tablist" aria-label="配置管理页面">
<li class="nav-item" role="presentation">
<a href="/assets" class="nav-link{{if eq .AssetTab "overview"}} active{{end}}" role="tab" {{if eq .AssetTab "overview"}}aria-selected="true"{{else}}aria-selected="false"{{end}}>总览</a>
</li>
<li class="nav-item" role="presentation">
<a href="/assets/video-sources" class="nav-link{{if eq .AssetTab "video-sources"}} active{{end}}" role="tab" {{if eq .AssetTab "video-sources"}}aria-selected="true"{{else}}aria-selected="false"{{end}}>视频源</a>
</li>
<li class="nav-item" role="presentation">
<a href="/assets/templates" class="nav-link{{if eq .AssetTab "templates"}} active{{end}}" role="tab" {{if eq .AssetTab "templates"}}aria-selected="true"{{else}}aria-selected="false"{{end}}>识别模板</a>
</li>
<li class="nav-item" role="presentation">
<a href="/assets/integrations" class="nav-link{{if eq .AssetTab "integrations"}} active{{end}}" role="tab" {{if eq .AssetTab "integrations"}}aria-selected="true"{{else}}aria-selected="false"{{end}}>第三方服务</a>
</li>
<li class="nav-item" role="presentation">
<a href="/assets/overlays" class="nav-link{{if eq .AssetTab "overlays"}} active{{end}}" role="tab" {{if eq .AssetTab "overlays"}}aria-selected="true"{{else}}aria-selected="false"{{end}}>调试参数</a>
</li>
</ul>
<div class="tab-content">
<div class="card tab-pane active show asset-tab-card">
<div class="card-body asset-panel-body">
{{end}}
{{define "asset_tabs_end"}}
</div>
</div>
</div>
{{end}}
{{define "assets"}}
{{template "asset_tabs" .}}
{{if eq .AssetTab "integrations"}}
<div class="card">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "service"}}<span>第三方服务列表</span></h2>
</div>
<div class="actions compact">
<a class="btn secondary" href="/assets/integrations?new=1">{{icon "apply"}}<span>新增服务</span></a>
{{if .AssetIntegration}}{{if .AssetIntegration.Name}}
<a class="btn secondary" href="/assets/integrations?name={{.AssetIntegration.Name}}&edit=1">{{icon "edit"}}<span>编辑</span></a>
<form method="post" action="/assets/integrations/{{.AssetIntegration.Name}}/delete" onsubmit="return confirm('确认删除服务 {{.AssetIntegration.Name}}')">
<button class="btn secondary" type="submit">删除</button>
</form>
{{end}}{{end}}
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>服务名称</th>
<th>服务类型</th>
<th>描述</th>
<th>地址摘要</th>
<th>状态</th>
<th>引用数量</th>
</tr>
</thead>
<tbody>
{{range .AssetIntegrations}}
<tr data-nav-row data-nav-href="/assets/integrations?name={{.Name}}"{{if and $.AssetIntegration (eq $.AssetIntegration.Name .Name)}} class="selected"{{end}}>
<td><a class="mono" href="/assets/integrations?name={{.Name}}">{{.Name}}</a></td>
<td>{{.TypeLabel}}</td>
<td>{{if .Description}}{{.Description}}{{else}}-{{end}}</td>
<td class="mono">{{if .AddressSummary}}{{.AddressSummary}}{{else}}-{{end}}</td>
<td>{{if .Enabled}}启用{{else}}停用{{end}}</td>
<td>{{.RefCount}}</td>
</tr>
{{else}}
<tr><td colspan="6"><div class="empty-state compact"><div class="empty-title">还没有第三方服务</div></div></td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{if .AssetIntegration}}
<form method="post" action="/assets/integrations">
<div class="card editor-state {{if .AssetIntegrationEditing}}editing{{else}}readonly{{end}}">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "service"}}<span>{{if .AssetIntegrationEditing}}第三方服务编辑{{else}}第三方服务详情{{end}}{{if .AssetIntegration.Name}} · {{.AssetIntegration.Name}}{{end}}</span></h2>
<div class="form-hint form-state-hint">
{{if .AssetIntegrationEditing}}
<span class="pill run">编辑模式</span>
<span>当前第三方服务正在编辑,保存后生效。</span>
{{else}}
<span class="pill">查看模式</span>
<span>当前第三方服务为只读展示。</span>
{{end}}
</div>
</div>
</div>
{{if .AssetIntegrationEditing}}
<div class="field-grid">
<label><span>服务名称<span class="required-mark">*</span></span><input name="name" value="{{.AssetIntegration.Name}}" pattern="[A-Za-z0-9_.-]+" title="只允许英文、数字和 ._- " required autofocus /></label>
<label>
<span>服务类型<span class="required-mark">*</span></span>
<select name="type" onchange="toggleIntegrationFields(this.value)">
<option value="object_storage" {{if eq .AssetIntegration.Type "object_storage"}}selected{{end}}>对象存储</option>
<option value="alarm_service" {{if eq .AssetIntegration.Type "alarm_service"}}selected{{end}}>告警服务</option>
<option value="custom" {{if eq .AssetIntegration.Type "custom"}}selected{{end}}>自定义</option>
</select>
</label>
<label><span>描述</span><input name="description" value="{{.AssetIntegration.Description}}" /></label>
<label><span>启用</span><select name="enabled"><option value="1" {{if .AssetIntegration.Enabled}}selected{{end}}>启用</option><option value="0" {{if not .AssetIntegration.Enabled}}selected{{end}}>停用</option></select></label>
</div>
<div class="field-group" data-type="object_storage">
<h3>对象存储配置</h3>
<div class="field-grid">
<label><span>对象存储地址</span><input class="mono" name="endpoint" value="{{if .AssetIntegration.ObjectStorage}}{{.AssetIntegration.ObjectStorage.Endpoint}}{{end}}" /></label>
<label><span>Bucket</span><input class="mono" name="bucket" value="{{if .AssetIntegration.ObjectStorage}}{{.AssetIntegration.ObjectStorage.Bucket}}{{end}}" /></label>
<label><span>Access Key</span><input class="mono" name="access_key" value="{{if .AssetIntegration.ObjectStorage}}{{.AssetIntegration.ObjectStorage.AccessKey}}{{end}}" /></label>
<label><span>Secret Key</span><input class="mono" name="secret_key" value="{{if .AssetIntegration.ObjectStorage}}{{.AssetIntegration.ObjectStorage.SecretKey}}{{end}}" /></label>
</div>
</div>
<div class="field-group" data-type="alarm_service">
<h3>告警服务配置</h3>
<div class="field-grid">
<label><span>Token 获取地址</span><input class="mono" name="get_token_url" value="{{if .AssetIntegration.AlarmService}}{{.AssetIntegration.AlarmService.GetTokenURL}}{{end}}" /></label>
<label><span>消息上报地址</span><input class="mono" name="put_message_url" value="{{if .AssetIntegration.AlarmService}}{{.AssetIntegration.AlarmService.PutMessageURL}}{{end}}" /></label>
<label><span>租户编码</span><input class="mono" name="tenant_code" value="{{if .AssetIntegration.AlarmService}}{{.AssetIntegration.AlarmService.TenantCode}}{{end}}" /></label>
</div>
</div>
<div class="field-group" data-type="custom">
<h3>自定义配置</h3>
<div class="field-grid">
<label class="full"><span>配置 JSON<span class="required-mark">*</span></span><textarea class="code-input" name="config_json" rows="10">{{.IntegrationConfigDraft}}</textarea></label>
</div>
</div>
<div class="actions">
<button type="submit">{{icon "apply"}}<span>保存</span></button>
<a class="btn secondary" href="/assets/integrations?name={{.AssetIntegration.Name}}">{{icon "close"}}<span>取消</span></a>
</div>
{{else}}
<div class="detail-sheet">
<div class="detail-item"><span>服务名称</span><strong class="mono">{{.AssetIntegration.Name}}</strong></div>
<div class="detail-item"><span>服务类型</span><strong>{{.AssetIntegration.TypeLabel}}</strong></div>
<div class="detail-item"><span>描述</span><strong>{{if .AssetIntegration.Description}}{{.AssetIntegration.Description}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>启用状态</span><strong>{{if .AssetIntegration.Enabled}}启用{{else}}停用{{end}}</strong></div>
{{if eq .AssetIntegration.Type "object_storage"}}
<div class="detail-item"><span>对象存储地址</span><strong class="mono">{{if and .AssetIntegration.ObjectStorage .AssetIntegration.ObjectStorage.Endpoint}}{{.AssetIntegration.ObjectStorage.Endpoint}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>Bucket</span><strong class="mono">{{if and .AssetIntegration.ObjectStorage .AssetIntegration.ObjectStorage.Bucket}}{{.AssetIntegration.ObjectStorage.Bucket}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>Access Key</span><strong class="mono">{{if and .AssetIntegration.ObjectStorage .AssetIntegration.ObjectStorage.AccessKey}}{{.AssetIntegration.ObjectStorage.AccessKey}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>Secret Key</span><strong class="mono">{{if and .AssetIntegration.ObjectStorage .AssetIntegration.ObjectStorage.SecretKey}}{{.AssetIntegration.ObjectStorage.SecretKey}}{{else}}-{{end}}</strong></div>
{{end}}
{{if eq .AssetIntegration.Type "alarm_service"}}
<div class="detail-item"><span>Token 获取地址</span><strong class="mono">{{if and .AssetIntegration.AlarmService .AssetIntegration.AlarmService.GetTokenURL}}{{.AssetIntegration.AlarmService.GetTokenURL}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>消息上报地址</span><strong class="mono">{{if and .AssetIntegration.AlarmService .AssetIntegration.AlarmService.PutMessageURL}}{{.AssetIntegration.AlarmService.PutMessageURL}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>租户编码</span><strong class="mono">{{if and .AssetIntegration.AlarmService .AssetIntegration.AlarmService.TenantCode}}{{.AssetIntegration.AlarmService.TenantCode}}{{else}}-{{end}}</strong></div>
{{end}}
</div>
{{end}}
</div>
</form>
{{if and (not .AssetIntegrationEditing) (eq .AssetIntegration.Type "custom")}}
<details class="card collapsible">
<summary class="title-with-icon">{{icon "tech"}}<span>原始 JSON</span></summary>
<pre>{{json .AssetIntegration.Raw}}</pre>
</details>
{{end}}
{{end}}
<script>
function toggleIntegrationFields(type) {
document.querySelectorAll('[data-type]').forEach(function(el) {
el.style.display = el.getAttribute('data-type') === type ? '' : 'none';
});
}
toggleIntegrationFields('{{.AssetIntegration.Type}}');
</script>
{{else if eq .AssetTab "video-sources"}}
{{if .Message}}<div class="msg">{{.Message}}</div>{{end}}
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
<div class="card">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "device"}}<span>视频源列表</span></h2>
</div>
<div class="actions compact">
<a class="btn secondary" href="/assets/video-sources/template" title="下载 CSV 模板">{{icon "apply"}}<span>模板下载</span></a>
<button class="btn secondary" type="button" onclick="document.getElementById('csv-import-input').click()">{{icon "apply"}}<span>CSV 导入</span></button>
<a class="btn secondary" href="/assets/video-sources?new=1">{{icon "apply"}}<span>新增视频源</span></a>
{{if .AssetVideoSource}}{{if .AssetVideoSource.Name}}
<a class="btn secondary" href="/assets/video-sources?name={{.AssetVideoSource.Name}}&edit=1">{{icon "edit"}}<span>编辑</span></a>
<form method="post" action="/assets/video-sources/{{.AssetVideoSource.ID}}/delete" onsubmit="return confirm('确认删除视频源 {{.AssetVideoSource.Name}}')">
<button class="btn secondary" type="submit">删除</button>
</form>
{{end}}{{end}}
</div>
<form id="csv-import-form" method="post" action="/assets/video-sources/import" enctype="multipart/form-data" style="display:none">
<input type="file" id="csv-import-input" name="file" accept=".csv" onchange="this.form.submit()">
</form>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>视频源名称</th>
<th>类型</th>
<th>区域</th>
<th>输入地址</th>
<th>分辨率</th>
<th>帧率</th>
<th>引用数量</th>
</tr>
</thead>
<tbody>
{{range .AssetVideoSources}}
<tr data-nav-row data-nav-href="/assets/video-sources?name={{.Name}}"{{if $.AssetVideoSource}}{{if eq $.AssetVideoSource.Name .Name}} class="selected"{{end}}{{end}}>
<td><a class="mono" href="/assets/video-sources?name={{.Name}}">{{.Name}}</a></td>
<td>{{.SourceTypeLabel}}</td>
<td>{{if .Area}}{{.Area}}{{else}}-{{end}}</td>
<td class="mono">{{if .Config.URL}}{{.Config.URL}}{{else}}-{{end}}</td>
<td>{{if .Config.Resolution}}{{.Config.Resolution}}{{else}}-{{end}}</td>
<td class="mono">{{if .Config.FPS}}{{.Config.FPS}}{{else}}-{{end}}</td>
<td>{{.RefCount}}</td>
</tr>
{{else}}
<tr><td colspan="7"><div class="empty-state compact"><div class="empty-title">还没有视频源</div></div></td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{if .AssetVideoSource}}
<form method="post" action="/assets/video-sources">
{{if .AssetVideoSource.ID}}<input type="hidden" name="id" value="{{.AssetVideoSource.ID}}">{{end}}
{{if .AssetVideoSource.Name}}<input type="hidden" name="original_name" value="{{.AssetVideoSource.Name}}">{{end}}
<div class="card editor-state {{if .AssetVideoSourceEditing}}editing{{else}}readonly{{end}}">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "device"}}<span>{{if .AssetVideoSourceEditing}}视频源编辑{{else}}视频源详情{{end}}{{if .AssetVideoSource.Name}} · {{.AssetVideoSource.Name}}{{end}}</span></h2>
<div class="form-hint form-state-hint">
{{if .AssetVideoSourceEditing}}
<span class="pill run">编辑模式</span>
<span>当前视频源正在编辑,保存后生效。</span>
{{else}}
<span class="pill">查看模式</span>
<span>当前视频源为只读展示。</span>
{{end}}
</div>
</div>
</div>
{{if .AssetVideoSourceEditing}}
<div class="field-grid">
<label><span>视频源名称<span class="required-mark">*</span></span><input name="name" value="{{.AssetVideoSource.Name}}" autofocus /></label>
<label>
<span>类型<span class="required-mark">*</span></span>
<select name="source_type">
<option value="rtsp" {{if eq .AssetVideoSource.SourceType "rtsp"}}selected{{end}}>RTSP</option>
<option value="rtmp" {{if eq .AssetVideoSource.SourceType "rtmp"}}selected{{end}}>RTMP</option>
<option value="file" {{if eq .AssetVideoSource.SourceType "file"}}selected{{end}}>文件</option>
<option value="usb_camera" {{if eq .AssetVideoSource.SourceType "usb_camera"}}selected{{end}}>USB 摄像头</option>
</select>
</label>
<label><span>区域</span><input name="area" value="{{.AssetVideoSource.Area}}" /></label>
<label><span>描述</span><input name="description" value="{{.AssetVideoSource.Description}}" /></label>
</div>
<div class="field-grid">
<label class="full"><span>输入地址<span class="required-mark">*</span></span><input class="mono" name="url" value="{{.AssetVideoSource.Config.URL}}" /></label>
<label><span>标准分辨率</span><input name="resolution" value="{{.AssetVideoSource.Config.Resolution}}" /></label>
<label><span>像素尺寸</span><input class="mono" name="frame_size" value="{{.AssetVideoSource.Config.FrameSize}}" /></label>
<label><span>帧率</span><input class="mono" name="fps" value="{{.AssetVideoSource.Config.FPS}}" /></label>
<label><span>视频格式</span><input name="video_format" value="{{.AssetVideoSource.Config.VideoFormat}}" /></label>
<label><span>焦距</span><input name="focal_length" value="{{.AssetVideoSource.Config.FocalLength}}" /></label>
<label><span>安装高度</span><input name="mount_height" value="{{.AssetVideoSource.Config.MountHeight}}" /></label>
<label><span>安装角度</span><input name="mount_angle" value="{{.AssetVideoSource.Config.MountAngle}}" /></label>
</div>
<div class="actions">
<button type="submit">{{icon "apply"}}<span>保存</span></button>
<a class="btn secondary" href="/assets/video-sources?name={{.AssetVideoSource.Name}}">{{icon "close"}}<span>取消</span></a>
</div>
{{else}}
<div class="detail-sheet">
<div class="detail-item"><span>视频源名称</span><strong class="mono">{{.AssetVideoSource.Name}}</strong></div>
<div class="detail-item"><span>类型</span><strong>{{.AssetVideoSource.SourceTypeLabel}}</strong></div>
<div class="detail-item"><span>区域</span><strong>{{if .AssetVideoSource.Area}}{{.AssetVideoSource.Area}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>描述</span><strong>{{if .AssetVideoSource.Description}}{{.AssetVideoSource.Description}}{{else}}-{{end}}</strong></div>
<div class="detail-item full"><span>输入地址</span><strong class="mono">{{if .AssetVideoSource.Config.URL}}{{.AssetVideoSource.Config.URL}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>标准分辨率</span><strong>{{if .AssetVideoSource.Config.Resolution}}{{.AssetVideoSource.Config.Resolution}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>像素尺寸</span><strong class="mono">{{if .AssetVideoSource.Config.FrameSize}}{{.AssetVideoSource.Config.FrameSize}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>帧率</span><strong class="mono">{{if .AssetVideoSource.Config.FPS}}{{.AssetVideoSource.Config.FPS}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>视频格式</span><strong>{{if .AssetVideoSource.Config.VideoFormat}}{{.AssetVideoSource.Config.VideoFormat}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>焦距</span><strong>{{if .AssetVideoSource.Config.FocalLength}}{{.AssetVideoSource.Config.FocalLength}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>安装高度</span><strong>{{if .AssetVideoSource.Config.MountHeight}}{{.AssetVideoSource.Config.MountHeight}}{{else}}-{{end}}</strong></div>
<div class="detail-item"><span>安装角度</span><strong>{{if .AssetVideoSource.Config.MountAngle}}{{.AssetVideoSource.Config.MountAngle}}{{else}}-{{end}}</strong></div>
</div>
{{end}}
</div>
</form>
{{end}}
{{else}}
<div class="stats">
<div class="stat accent-teal">
<div class="k metric-label">{{icon "template"}}<span>识别模板</span></div>
<div class="v">{{len .AssetTemplates}}</div>
<div class="hint">{{if .ConfigSources.Root}}{{.ConfigSources.Root}}{{else}}标准模板与配置均存储在本地数据库{{end}}</div>
</div>
<div class="stat accent-green">
<div class="k metric-label">{{icon "device"}}<span>视频源</span></div>
<div class="v">{{len .AssetVideoSources}}</div>
<div class="hint">可复用输入流配置</div>
</div>
<div class="stat accent-slate">
<div class="k metric-label">{{icon "overlay"}}<span>调试参数</span></div>
<div class="v">{{len .AssetOverlays}}</div>
<div class="hint">调试与敏感度变化</div>
</div>
<div class="stat accent-amber">
<div class="k metric-label">{{icon "service"}}<span>第三方服务</span></div>
<div class="v">{{len .AssetIntegrations}}</div>
<div class="hint">告警、对象存储和认证服务</div>
</div>
</div>
<div class="quad-grid">
<div class="card">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "template"}}<span>识别模板</span></h2>
</div>
</div>
<div class="asset-list">
{{range .AssetTemplates}}
<a class="asset-row asset-link" href="/assets/templates/{{.Name}}">
<span class="mono">{{.Name}}</span>
<span class="muted small">{{.NodeCount}}节点 / {{.EdgeCount}}连线</span>
</a>
{{else}}
<div class="empty-state compact">
<div class="empty-title">还没有模板</div>
</div>
{{end}}
</div>
</div>
<div class="card">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "device"}}<span>视频源</span></h2>
</div>
</div>
<div class="asset-list">
{{range .AssetVideoSources}}
<a class="asset-row asset-link" href="/assets/video-sources?name={{.Name}}">
<span class="mono">{{.Name}}</span>
<span class="muted small">{{if .Area}}{{.Area}} / {{end}}{{if .Config.Resolution}}{{.Config.Resolution}}{{else}}未设置分辨率{{end}}</span>
</a>
{{else}}
<div class="empty-state compact">
<div class="empty-title">还没有视频源</div>
</div>
{{end}}
</div>
</div>
<div class="card">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "service"}}<span>第三方服务</span></h2>
</div>
</div>
<div class="asset-list">
{{range .AssetIntegrations}}
<a class="asset-row asset-link" href="/assets/integrations">
<span>{{.Name}}</span>
<span class="muted small">{{if .AddressSummary}}{{.AddressSummary}}{{else}}{{.TypeLabel}}{{end}}</span>
</a>
{{else}}
<div class="empty-state compact">
<div class="empty-title">还没有第三方服务</div>
</div>
{{end}}
</div>
</div>
<div class="card">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "overlay"}}<span>调试参数</span></h2>
</div>
</div>
<div class="asset-list">
{{range .AssetOverlays}}
<a class="asset-row asset-link" href="/assets/overlays/{{.Name}}">
<span>{{.Name}}</span>
<span class="muted small">{{.OverrideTargetNum}} 个目标</span>
</a>
{{else}}
<div class="empty-state compact">
<div class="empty-title">还没有调试参数</div>
</div>
{{end}}
</div>
</div>
</div>
{{end}}
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
{{template "asset_tabs_end" .}}
{{end}}