feat: 视频源管理页增加 CSV 模板下载与批量导入
This commit is contained in:
parent
2369a1e3dc
commit
08ec88c45e
@ -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 = "配置中心"
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user