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

407 lines
22 KiB
HTML

{{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.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">
<button class="btn secondary" type="submit">删除</button>
</form>
{{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"}}
<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.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.Name}}/delete">
<button class="btn secondary" type="submit">删除</button>
</form>
{{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 and $.AssetVideoSource (eq $.AssetVideoSource.Name .Name)}} class="selected"{{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">
<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}}