This commit is contained in:
toom1996
2026-09-29 19:59:30 +08:00
parent d15d2a4701
commit 361ffc01af
28 changed files with 896 additions and 47 deletions

View File

@ -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,

View File

@ -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).

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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)
}
}
}