feat: 视频源管理页增加 CSV 模板下载与批量导入

This commit is contained in:
tian 2026-07-20 10:52:25 +08:00
parent 2369a1e3dc
commit 08ec88c45e
2 changed files with 107 additions and 0 deletions

View File

@ -3,6 +3,7 @@ package web
import (
"bytes"
"crypto/sha256"
"encoding/csv"
"encoding/hex"
"encoding/json"
"fmt"
@ -748,6 +749,8 @@ func (u *UI) Routes() (chi.Router, error) {
r.Post("/device-assignments/{id}/delete", u.actionDeviceAssignmentDelete)
r.Get("/assets", u.pageAssets)
r.Get("/assets/video-sources", u.pageAssetVideoSources)
r.Get("/assets/video-sources/template", u.actionVideoSourceCSVTemplate)
r.Post("/assets/video-sources/import", u.actionVideoSourceCSVImport)
r.Post("/assets/video-sources", u.actionAssetVideoSourceSave)
r.Post("/assets/video-sources/{name}/delete", u.actionAssetVideoSourceDelete)
r.Get("/assets/templates", u.pageAssetTemplates)
@ -3522,6 +3525,105 @@ func (u *UI) actionAssetVideoSourceDelete(w http.ResponseWriter, r *http.Request
http.Redirect(w, r, "/ui/assets/video-sources?msg="+urlQueryEscape("视频源已删除"), http.StatusFound)
}
func (u *UI) actionVideoSourceCSVTemplate(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=video_sources_template.csv")
w.Write([]byte("\xef\xbb\xbf")) // BOM for Excel
w.Write([]byte("name,source_type,area,url,resolution,fps,description\n"))
w.Write([]byte("车间东门,rtsp,车间A,rtsp://192.168.1.100:554/stream1,1920x1080,25,\n"))
w.Write([]byte("车间西门,rtsp,车间A,rtsp://192.168.1.101:554/stream1,1920x1080,25,\n"))
}
func (u *UI) actionVideoSourceCSVImport(w http.ResponseWriter, r *http.Request) {
if u.preview == nil {
http.Error(w, "not available", http.StatusServiceUnavailable)
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
http.Redirect(w, r, "/ui/assets/video-sources?error="+urlQueryEscape("文件解析失败"), http.StatusFound)
return
}
file, _, err := r.FormFile("file")
if err != nil {
http.Redirect(w, r, "/ui/assets/video-sources?error="+urlQueryEscape("请选择文件"), http.StatusFound)
return
}
defer file.Close()
reader := csv.NewReader(file)
reader.LazyQuotes = true
records, err := reader.ReadAll()
if err != nil {
http.Redirect(w, r, "/ui/assets/video-sources?error="+urlQueryEscape("CSV 解析失败: "+err.Error()), http.StatusFound)
return
}
if len(records) < 2 {
http.Redirect(w, r, "/ui/assets/video-sources?error="+urlQueryEscape("CSV 文件为空或无数据行"), http.StatusFound)
return
}
imported := 0
skipped := 0
for i, row := range records {
if i == 0 {
continue // skip header
}
if len(row) < 4 {
skipped++
continue
}
name := strings.TrimSpace(row[0])
sourceType := strings.TrimSpace(row[1])
area := ""
url := ""
resolution := ""
fps := ""
if len(row) > 2 {
area = strings.TrimSpace(row[2])
}
if len(row) > 3 {
url = strings.TrimSpace(row[3])
}
if len(row) > 4 {
resolution = strings.TrimSpace(row[4])
}
if len(row) > 5 {
fps = strings.TrimSpace(row[5])
}
if name == "" || url == "" {
skipped++
continue
}
if sourceType == "" {
sourceType = "rtsp"
}
if resolution == "" {
resolution = "1920x1080"
}
if fps == "" {
fps = "25"
}
asset := service.ConfigVideoSourceAsset{
Name: name,
SourceType: sourceType,
Area: area,
Config: service.VideoSourceConfig{
URL: url,
Resolution: resolution,
FPS: fps,
},
}
if err := u.preview.SaveVideoSourceAsset(asset); err != nil {
skipped++
continue
}
imported++
}
msg := fmt.Sprintf("导入完成:%d 个视频源,%d 条跳过", imported, skipped)
http.Redirect(w, r, "/ui/assets/video-sources?msg="+urlQueryEscape(msg), http.StatusFound)
}
func (u *UI) pageAssetIntegrations(w http.ResponseWriter, r *http.Request) {
data := u.assetPageData("integrations")
data.Title = "配置中心"

View File

@ -183,6 +183,8 @@ toggleIntegrationFields('{{.AssetIntegration.Type}}');
<h2 class="title-with-icon">{{icon "device"}}<span>视频源列表</span></h2>
</div>
<div class="actions compact">
<a class="btn secondary" href="/ui/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="/ui/assets/video-sources?new=1">{{icon "apply"}}<span>新增视频源</span></a>
{{if .AssetVideoSource.Name}}
<a class="btn secondary" href="/ui/assets/video-sources?name={{.AssetVideoSource.Name}}&edit=1">{{icon "edit"}}<span>编辑</span></a>
@ -191,6 +193,9 @@ toggleIntegrationFields('{{.AssetIntegration.Type}}');
</form>
{{end}}
</div>
<form id="csv-import-form" method="post" action="/ui/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>