update
This commit is contained in:
@ -249,7 +249,8 @@ func (h *BackstageHandler) RunwayDetail(c *gin.Context) {
|
||||
renderPage(c, http.StatusNotFound, "error", gin.H{"Error": "走秀不存在或已删除: " + raw})
|
||||
return
|
||||
}
|
||||
renderPage(c, http.StatusOK, "runway-detail", gin.H{"Article": art})
|
||||
total, _ := h.article.SumImageSize(c.Request.Context(), numeric)
|
||||
renderPage(c, http.StatusOK, "runway-detail", gin.H{"Article": art, "ImageTotalSize": total})
|
||||
}
|
||||
|
||||
// StreetSnaps 街拍管理列表:按年份/城市/排序筛选,复用 StreetSnapService.AdminList(读基表,含待审)。
|
||||
@ -305,7 +306,8 @@ func (h *BackstageHandler) StreetSnapDetail(c *gin.Context) {
|
||||
renderPage(c, http.StatusNotFound, "error", gin.H{"Error": "街拍不存在或已删除: " + raw})
|
||||
return
|
||||
}
|
||||
renderPage(c, http.StatusOK, "street-snap-detail", gin.H{"Snap": snap})
|
||||
total, _ := h.street.SumImageSize(c.Request.Context(), numeric)
|
||||
renderPage(c, http.StatusOK, "street-snap-detail", gin.H{"Snap": snap, "ImageTotalSize": total})
|
||||
}
|
||||
|
||||
// ── 文章管理(走秀 + 街拍混合列表,替代原审核页与分类型列表)─────────────
|
||||
@ -327,6 +329,8 @@ func (h *BackstageHandler) Articles(c *gin.Context) {
|
||||
}
|
||||
// 批量统计每篇图集的重复率(基于 is_duplicate 列,阈值 phash.DefaultThreshold=4)。
|
||||
dupStats := h.dupStatsByCards(c.Request.Context(), cards)
|
||||
// 批量统计每篇图集的原图总大小(基于 image_size 列,worker 下载时写入源文件字节数)。
|
||||
imageSizes := h.imageSizeByCards(c.Request.Context(), cards)
|
||||
items := make([]gin.H, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
var t byte
|
||||
@ -347,6 +351,7 @@ func (h *BackstageHandler) Articles(c *gin.Context) {
|
||||
"Title": card.Title,
|
||||
"Subtitle": card.Subtitle,
|
||||
"ImageCount": card.ImageCount,
|
||||
"ImageTotalSize": imageSizes[card.ID],
|
||||
"Status": card.Status,
|
||||
"DupCount": ds.Dup,
|
||||
"DupPct": dupPct,
|
||||
@ -407,6 +412,36 @@ func (h *BackstageHandler) dupStatsByCards(ctx context.Context, cards []reposito
|
||||
return out
|
||||
}
|
||||
|
||||
// imageSizeByCards 按卡片类型批量取每篇图集的原图字节数之和(image_size 列)。
|
||||
// 按 kind 分别查对应图片表,避免跨表混算;查询失败时该 kind 整体回落为「无大小」(不阻断列表)。
|
||||
func (h *BackstageHandler) imageSizeByCards(ctx context.Context, cards []repository.AdminRecordCard) map[uint32]int64 {
|
||||
out := make(map[uint32]int64)
|
||||
runwayIDs := make([]uint32, 0, len(cards))
|
||||
streetIDs := make([]uint32, 0, len(cards))
|
||||
for _, c := range cards {
|
||||
if c.Kind == "runway" {
|
||||
runwayIDs = append(runwayIDs, c.ID)
|
||||
} else {
|
||||
streetIDs = append(streetIDs, c.ID)
|
||||
}
|
||||
}
|
||||
if len(runwayIDs) > 0 {
|
||||
if m, err := h.adminRepo.ImageSizeByRecords(ctx, "runway", runwayIDs); err == nil {
|
||||
for id, s := range m {
|
||||
out[id] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(streetIDs) > 0 {
|
||||
if m, err := h.adminRepo.ImageSizeByRecords(ctx, "street", streetIDs); err == nil {
|
||||
for id, s := range m {
|
||||
out[id] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ArticleApprove 通过:把记录状态置为已发布(单表模型:仅改状态,不重建图片)。
|
||||
// 路由标识用 hashid,与编辑页一致;已发布 / 已驳回记录调用仍是幂等的。
|
||||
func (h *BackstageHandler) ArticleApprove(c *gin.Context) {
|
||||
@ -684,12 +719,17 @@ func (h *BackstageHandler) RunwayEdit(c *gin.Context) {
|
||||
dupPct = float64(dupCount) / float64(n) * 100
|
||||
}
|
||||
dupSources := h.buildDupSources(c.Request.Context(), "runway", cells)
|
||||
var totalSize int64
|
||||
for _, im := range imgs {
|
||||
totalSize += im.ImageSize
|
||||
}
|
||||
renderPage(c, http.StatusOK, "runway-edit", gin.H{
|
||||
"UID": raw,
|
||||
"Kind": "runway",
|
||||
"RecordID": rw.ID,
|
||||
"Status": rw.Status,
|
||||
"Runway": rw,
|
||||
"ImageTotalSize": totalSize,
|
||||
"Images": imgs,
|
||||
"Cells": cells,
|
||||
"BrandName": brandName,
|
||||
@ -869,12 +909,17 @@ func (h *BackstageHandler) StreetSnapEdit(c *gin.Context) {
|
||||
dupPct = float64(dupCount) / float64(n) * 100
|
||||
}
|
||||
dupSources := h.buildDupSources(c.Request.Context(), "street", cells)
|
||||
var totalSize int64
|
||||
for _, im := range imgs {
|
||||
totalSize += im.ImageSize
|
||||
}
|
||||
renderPage(c, http.StatusOK, "snap-edit", gin.H{
|
||||
"UID": raw,
|
||||
"Kind": "street",
|
||||
"RecordID": snap.ID,
|
||||
"Status": snap.Status,
|
||||
"Snap": snap,
|
||||
"ImageTotalSize": totalSize,
|
||||
"Images": imgs,
|
||||
"Cells": cells,
|
||||
"IsDown": snap.IsDeleted == 1,
|
||||
|
||||
@ -19,6 +19,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
@ -41,6 +42,25 @@ func SetImgComposer(c *imgurl.Composer) {
|
||||
}
|
||||
}
|
||||
|
||||
// formatBytes 把字节数格式化为人类可读串(B/KB/MB/GB/TB);0 或负数返回「—」。
|
||||
func formatBytes(n int64) string {
|
||||
if n <= 0 {
|
||||
return "—"
|
||||
}
|
||||
const unit = 1024
|
||||
if n < unit {
|
||||
return fmt.Sprintf("%d B", n)
|
||||
}
|
||||
val := float64(n)
|
||||
for _, u := range []string{"KB", "MB", "GB", "TB"} {
|
||||
val /= unit
|
||||
if val < unit {
|
||||
return fmt.Sprintf("%.1f %s", val, u)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%.1f PB", val)
|
||||
}
|
||||
|
||||
//go:embed templates/layout.html templates/partials.html templates/pages/*.html assets/*
|
||||
var backstageFS embed.FS
|
||||
|
||||
@ -60,6 +80,7 @@ func init() {
|
||||
// imgThumb 同但走列表缩略图样式(小尺寸场景省流量)。两者兜底逻辑见 imgurl.Composer。
|
||||
"imgURL": func(stored string) string { return imgComposer.Compose(stored) },
|
||||
"imgThumb": func(stored string) string { return imgComposer.ComposeThumb(stored) },
|
||||
"formatBytes": formatBytes,
|
||||
}
|
||||
|
||||
base, err := template.New("layout.html").Funcs(funcs).
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:30px"><input type="checkbox" id="sel-all" aria-label="全选本页"></th>
|
||||
<th>类型</th><th>ID</th><th>标题</th><th>副标题</th><th>图数</th><th>重复率</th><th>状态</th><th>操作</th>
|
||||
<th>类型</th><th>ID</th><th>标题</th><th>副标题</th><th>图数</th><th>原图总大小</th><th>重复率</th><th>状态</th><th>操作</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{{range .Items}}<tr data-kind="{{.Kind}}" data-id="{{.ID}}">
|
||||
@ -30,7 +30,7 @@
|
||||
<td><code>{{.ID}}</code></td>
|
||||
<td>{{.Title}}</td>
|
||||
<td class="muted">{{.Subtitle}}</td>
|
||||
<td>{{.ImageCount}}</td>
|
||||
<td>{{.ImageCount}}</td><td>{{formatBytes .ImageTotalSize}}</td>
|
||||
<td>{{if .DupCount}}<span class="badge dup">{{printf "%.0f" .DupPct}}%({{.DupCount}})</span>{{else}}<span class="muted">—</span>{{end}}</td>
|
||||
<td><span class="badge {{.Status}}">{{.Status}}</span></td>
|
||||
<td>
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
<div class="main">
|
||||
<p class="muted">编码 {{.Article.UID}} · 品牌 {{.Article.BrandName}}</p>
|
||||
<h1>{{.Article.Title}}</h1>
|
||||
<p class="muted">原图总大小:{{formatBytes .ImageTotalSize}}</p>
|
||||
<div class="grid">
|
||||
{{range .Article.Images}}<div class="cell"><button class="thumb" type="button" data-zoom><img src="{{imgURL .Image}}" loading="lazy"></button><span>{{.Name}}</span></div>{{range .Detail}}<div class="cell det"><button class="thumb" type="button" data-zoom><img src="{{imgURL .Image}}" loading="lazy"></button><span>{{.Name}} · 副图</span></div>{{end}}{{end}}
|
||||
</div>
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
{{template "bar" .}}
|
||||
<div class="main">
|
||||
<p class="muted">品牌 {{.BrandName}}{{if .IsDown}} · <span class="down">已下架</span>{{end}}</p>
|
||||
<p class="muted">原图总大小:{{formatBytes .ImageTotalSize}}</p>
|
||||
<form method="post" action="/admin/runways/{{.UID}}/edit">
|
||||
<div class="row"><div><label>英文标题</label><input name="title_en" value="{{.Runway.TitleEn}}"></div><div><label>中文标题</label><input name="title_cn" value="{{.Runway.TitleCn}}"></div></div>
|
||||
<div class="row"><div><label>英文描述</label><textarea name="description_en">{{.Runway.DescriptionEn}}</textarea></div><div><label>中文描述</label><textarea name="description_cn">{{.Runway.DescriptionCn}}</textarea></div></div>
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
{{template "bar" .}}
|
||||
<div class="main">
|
||||
<p class="muted">{{if .IsDown}}<span class="down">已下架</span>{{end}}</p>
|
||||
<p class="muted">原图总大小:{{formatBytes .ImageTotalSize}}</p>
|
||||
<form method="post" action="/admin/street-snaps/{{.UID}}/edit">
|
||||
<div class="row"><div><label>英文标题</label><input name="title" value="{{.Snap.Title}}"></div><div><label>中文标题</label><input name="title_cn" value="{{.Snap.TitleCn}}"></div></div>
|
||||
<div class="row"><div><label>年份</label><input name="year" value="{{.Snap.Year}}"></div><div><label>城市</label><input name="city" value="{{.Snap.City}}"></div></div>
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
<div class="main">
|
||||
<p class="muted">编码 {{.Snap.UID}}</p>
|
||||
<h1>{{.Snap.Title}}</h1>
|
||||
<p class="muted">原图总大小:{{formatBytes .ImageTotalSize}}</p>
|
||||
<div class="grid">
|
||||
{{range .Snap.Images}}<div class="cell"><button class="thumb" type="button" data-zoom><img src="{{imgURL .Image}}" loading="lazy"></button><span>{{.Name}}</span></div>{{range .Detail}}<div class="cell det"><button class="thumb" type="button" data-zoom><img src="{{imgURL .Image}}" loading="lazy"></button><span>{{.Name}} · 副图</span></div>{{end}}{{end}}
|
||||
</div>
|
||||
|
||||
25
internal/handler/templates_format_test.go
Normal file
25
internal/handler/templates_format_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package handler
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestFormatBytes 校验后台「图集原图总大小」的字节格式化(0/负数为「—」)。
|
||||
func TestFormatBytes(t *testing.T) {
|
||||
cases := []struct {
|
||||
n int64
|
||||
want string
|
||||
}{
|
||||
{0, "—"},
|
||||
{-1, "—"},
|
||||
{500, "500 B"},
|
||||
{1024, "1.0 KB"},
|
||||
{1536, "1.5 KB"},
|
||||
{1024 * 1024, "1.0 MB"},
|
||||
{int64(1.5 * 1024 * 1024), "1.5 MB"},
|
||||
{int64(1.5 * 1024 * 1024 * 1024), "1.5 GB"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := formatBytes(c.n); got != c.want {
|
||||
t.Errorf("formatBytes(%d) = %q, want %q", c.n, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user