refactor(publish): 审核侧切到正式表,通过即改状态不再晋升

This commit is contained in:
toom1996
2026-09-23 11:09:12 +08:00
parent e0ce81318d
commit 76626b8372
19 changed files with 423 additions and 775 deletions

View File

@ -240,7 +240,8 @@ tbody tr:last-child td { border-bottom: 0; }
}
.badge.pending { background: #fff4e0; color: #a85b00; }
.badge.processing { background: #eef2ff; color: #3b4bb5; }
.badge.approved, .badge.done { background: #e6f7ec; color: #1a7f43; }
/* published 是单表发布模型的「已通过」(旧值 approved 已被 published 取代)。 */
.badge.published, .badge.approved, .badge.done { background: #e6f7ec; color: #1a7f43; }
.badge.rejected, .badge.failed { background: #fdeaea; color: #c0392b; }
.badge.dup { background: #f3e8ff; color: #6b21a8; }
.badge.free { background: #eef2ff; color: #3b4bb5; }

View File

@ -32,7 +32,7 @@ type reviewBatchReq struct {
Items []reviewBatchItem `json:"items"`
}
// ReviewBatch 批量通过 / 驳回审核草稿。
// ReviewBatch 批量通过 / 驳回审核记录。
//
// 请求体:{"action":"approve","items":[{"kind":"street","id":12}, ...]}
// 响应: {"ok":2,"failed":[{"id":9,"error":"..."}],"pending":17}
@ -49,7 +49,7 @@ func (h *BackstageHandler) ReviewBatch(c *gin.Context) {
return
}
if len(req.Items) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "没有选中任何草稿"})
c.JSON(http.StatusBadRequest, gin.H{"error": "没有选中任何记录"})
return
}
if len(req.Items) > maxReviewBatch {
@ -99,9 +99,10 @@ type reviewAttachReq struct {
Imgs []uint32 `json:"imgs"`
}
// ReviewAttachImages 把多张草稿图并入指定主图(街拍审核页「合并选中为一组」的 AJAX 版)。
// ReviewAttachImagesAPI 把多张图并入指定主图(街拍审核页「合并选中为一组」的 AJAX 版)。
// 响应带回最新的图片分组投影,前端据此就地更新标签,无需整页刷新。
func (h *BackstageHandler) ReviewAttachImages(c *gin.Context) {
// 加 API 后缀以区别于表单版 ReviewAttachImages:本方法收发 JSON,表单版返回 302。
func (h *BackstageHandler) ReviewAttachImagesAPI(c *gin.Context) {
kind := c.Param("kind")
id := atoiDefault(c.Param("id"), 0)
if id == 0 {
@ -121,15 +122,15 @@ func (h *BackstageHandler) ReviewAttachImages(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "请先勾选要合并的图片"})
return
}
if err := h.review.AttachDraftImages(c.Request.Context(), kind, uint32(id), req.Main, req.Imgs); err != nil {
if err := h.review.AttachImages(c.Request.Context(), kind, uint32(id), req.Main, req.Imgs); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.respondDraftImages(c, kind, uint32(id))
h.respondRecordImages(c, kind, uint32(id))
}
// ReviewDetachImage 把副图拆出恢复为主图(AJAX 版),响应同 ReviewAttachImages。
func (h *BackstageHandler) ReviewDetachImage(c *gin.Context) {
// ReviewDetachImageAPI 把副图拆出恢复为主图(AJAX 版),响应同 ReviewAttachImagesAPI。
func (h *BackstageHandler) ReviewDetachImageAPI(c *gin.Context) {
kind := c.Param("kind")
id := atoiDefault(c.Param("id"), 0)
imgID := atoiDefault(c.Param("img"), 0)
@ -137,18 +138,18 @@ func (h *BackstageHandler) ReviewDetachImage(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"})
return
}
if err := h.review.DetachDraftImage(c.Request.Context(), kind, uint32(id), uint32(imgID)); err != nil {
if err := h.review.DetachImage(c.Request.Context(), kind, uint32(id), uint32(imgID)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.respondDraftImages(c, kind, uint32(id))
h.respondRecordImages(c, kind, uint32(id))
}
// respondDraftImages 重新读取草稿并把「图片分组状态」按前端需要的形状返回。
// respondRecordImages 重新读取记录并把「图片分组状态」按前端需要的形状返回。
//
// 投影只带前端更新标签必需的字段,不重复返回图片 URL(页面上的 <img> 已经有了)。
func (h *BackstageHandler) respondDraftImages(c *gin.Context, kind string, id uint32) {
view, err := h.review.DraftDetail(c.Request.Context(), kind, id)
func (h *BackstageHandler) respondRecordImages(c *gin.Context, kind string, id uint32) {
view, err := h.review.RecordDetail(c.Request.Context(), kind, id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return

View File

@ -304,9 +304,9 @@ func (h *BackstageHandler) StreetSnapDetail(c *gin.Context) {
renderPage(c, http.StatusOK, "street-snap-detail", gin.H{"Snap": snap})
}
// ── 审核草稿 ───────────────────────────────────────────────────────────────────
// ── 审核记录 ───────────────────────────────────────────────────────────────────
// ReviewList 待审草稿列表:按类型(kind,缺省全部)与状态筛选。
// ReviewList 待审记录列表:按类型(kind,缺省全部)与状态筛选。
// 类型标签由审核注册表生成(见 ReviewService.KindTabs),新增模块自动出现,无需改此分支。
func (h *BackstageHandler) ReviewList(c *gin.Context) {
kind := c.Param("kind")
@ -329,7 +329,7 @@ func (h *BackstageHandler) ReviewList(c *gin.Context) {
if kind != "" && kind != "all" && !registered[kind] {
kind = ""
}
drafts, total, err := h.review.ListDrafts(c.Request.Context(), kind, status, page, 20)
records, total, err := h.review.ListRecords(c.Request.Context(), kind, status, page, 20)
if err != nil {
renderPage(c, http.StatusInternalServerError, "error", gin.H{"Error": err.Error()})
return
@ -342,7 +342,7 @@ func (h *BackstageHandler) ReviewList(c *gin.Context) {
p := pagination.Params{Page: page, PageSize: 20}
meta := p.NewMeta(total)
renderPage(c, http.StatusOK, "review-list", gin.H{
"Drafts": drafts,
"Records": records,
"Total": total,
"Status": status,
"Kind": kind,
@ -358,23 +358,23 @@ func (h *BackstageHandler) ReviewList(c *gin.Context) {
})
}
// ReviewDetail 草稿详情:展示预览 + 可编辑字段 + 通过/驳回操作。
// ReviewDetail 记录详情:展示预览 + 可编辑字段 + 通过/驳回操作。
func (h *BackstageHandler) ReviewDetail(c *gin.Context) {
kind := c.Param("kind")
id := atoiDefault(c.Param("id"), 0)
if id == 0 {
renderPage(c, http.StatusNotFound, "error", gin.H{"Error": "草稿不存在: " + c.Param("id")})
renderPage(c, http.StatusNotFound, "error", gin.H{"Error": "记录不存在: " + c.Param("id")})
return
}
draft, err := h.review.DraftDetail(c.Request.Context(), kind, uint32(id))
record, err := h.review.RecordDetail(c.Request.Context(), kind, uint32(id))
if err != nil {
renderPage(c, http.StatusNotFound, "error", gin.H{"Error": "草稿不存在或已删除: " + c.Param("id")})
renderPage(c, http.StatusNotFound, "error", gin.H{"Error": "记录不存在或已删除: " + c.Param("id")})
return
}
// 有品牌归属的模块(如走秀)解析品牌名;其余模块 BrandID=0 跳过。
brandName := ""
if draft.BrandID > 0 {
if b, berr := h.brand.Get(c.Request.Context(), hashid.Encode(draft.BrandID)); berr == nil {
if record.BrandID > 0 {
if b, berr := h.brand.Get(c.Request.Context(), hashid.Encode(record.BrandID)); berr == nil {
brandName = b.NameEn
if brandName == "" {
brandName = b.NameCn
@ -382,21 +382,21 @@ func (h *BackstageHandler) ReviewDetail(c *gin.Context) {
}
}
renderPage(c, http.StatusOK, "review-detail", gin.H{
"Draft": draft,
"Record": record,
"BrandName": brandName,
})
}
// ReviewApprove 通过:先应用审核人的字段微调,再晋升到正式表。
// ReviewApprove 通过:先应用审核人的字段微调,再把记录状态置为已发布(单表模型:不再晋升重建)。
func (h *BackstageHandler) ReviewApprove(c *gin.Context) {
kind := c.Param("kind")
id := atoiDefault(c.Param("id"), 0)
if id == 0 {
renderPage(c, http.StatusNotFound, "error", gin.H{"Error": "草稿不存在: " + c.Param("id")})
renderPage(c, http.StatusNotFound, "error", gin.H{"Error": "记录不存在: " + c.Param("id")})
return
}
fields := draftFormFields(c)
if err := h.review.UpdateDraft(c.Request.Context(), kind, uint32(id), fields); err != nil {
fields := reviewFormFields(c)
if err := h.review.UpdateRecord(c.Request.Context(), kind, uint32(id), fields); err != nil {
renderPage(c, http.StatusInternalServerError, "error", gin.H{"Error": err.Error()})
return
}
@ -413,7 +413,7 @@ func (h *BackstageHandler) ReviewReject(c *gin.Context) {
kind := c.Param("kind")
id := atoiDefault(c.Param("id"), 0)
if id == 0 {
renderPage(c, http.StatusNotFound, "error", gin.H{"Error": "草稿不存在: " + c.Param("id")})
renderPage(c, http.StatusNotFound, "error", gin.H{"Error": "记录不存在: " + c.Param("id")})
return
}
reason := c.PostForm("reason")
@ -425,9 +425,9 @@ func (h *BackstageHandler) ReviewReject(c *gin.Context) {
c.Redirect(http.StatusFound, reviewListURL(kind))
}
// ReviewDraftImageDelete 软删审核草稿里的单张图片(审核阶段剔除坏图,不删S4文件)。
// 校验 kind 合法后委派给审核服务,成功后跳回草稿详情页。
func (h *BackstageHandler) ReviewDraftImageDelete(c *gin.Context) {
// ReviewImageDelete 软删审核记录里的单张图片(审核阶段剔除坏图,不删S4文件)。
// 校验 kind 合法后委派给审核服务,成功后跳回记录详情页。
func (h *BackstageHandler) ReviewImageDelete(c *gin.Context) {
kind := c.Param("kind")
id := atoiDefault(c.Param("id"), 0)
imgID := atoiDefault(c.Param("img"), 0)
@ -435,16 +435,17 @@ func (h *BackstageHandler) ReviewDraftImageDelete(c *gin.Context) {
renderPage(c, http.StatusBadRequest, "error", gin.H{"Error": "参数无效"})
return
}
if err := h.review.DeleteDraftImage(c.Request.Context(), kind, uint32(id), uint32(imgID)); err != nil {
if err := h.review.DeleteImage(c.Request.Context(), kind, uint32(id), uint32(imgID)); err != nil {
renderPage(c, http.StatusInternalServerError, "error", gin.H{"Error": err.Error()})
return
}
c.Redirect(http.StatusFound, "/admin/reviews/"+kind+"/"+c.Param("id"))
}
// ReviewAttachDraftImages 把勾选的草稿图并入指定主图(街拍审核页「合并选中为一组」)。
// ReviewAttachImages 把勾选的图并入指定主图(街拍审核页「合并选中为一组」的表单版)。
// 主图由表单里的单选 name="main" 指定;待并入的图由多个 checkbox name="img" 提交。
func (h *BackstageHandler) ReviewAttachDraftImages(c *gin.Context) {
// (AJAX 版同名的 JSON 接口是 ReviewAttachImagesAPI —— 两者响应形态不同,故不能合并成一个。)
func (h *BackstageHandler) ReviewAttachImages(c *gin.Context) {
kind := c.Param("kind")
id := atoiDefault(c.Param("id"), 0)
if id == 0 {
@ -466,15 +467,15 @@ func (h *BackstageHandler) ReviewAttachDraftImages(c *gin.Context) {
renderPage(c, http.StatusBadRequest, "error", gin.H{"Error": "请在要作为主图的图片上点选「主图」"})
return
}
if err := h.review.AttachDraftImages(c.Request.Context(), kind, uint32(id), mainID, imgIDs); err != nil {
if err := h.review.AttachImages(c.Request.Context(), kind, uint32(id), mainID, imgIDs); err != nil {
renderPage(c, http.StatusInternalServerError, "error", gin.H{"Error": err.Error()})
return
}
c.Redirect(http.StatusFound, reviewDetailURL(kind, uint32(id)))
}
// ReviewDetachDraftImage 把副图拆出恢复为主图。
func (h *BackstageHandler) ReviewDetachDraftImage(c *gin.Context) {
// ReviewDetachImage 把副图拆出恢复为主图(表单版;AJAX 版见 ReviewDetachImageAPI)。
func (h *BackstageHandler) ReviewDetachImage(c *gin.Context) {
kind := c.Param("kind")
id := atoiDefault(c.Param("id"), 0)
imgID := atoiDefault(c.Param("img"), 0)
@ -482,7 +483,7 @@ func (h *BackstageHandler) ReviewDetachDraftImage(c *gin.Context) {
renderPage(c, http.StatusBadRequest, "error", gin.H{"Error": "参数无效"})
return
}
if err := h.review.DetachDraftImage(c.Request.Context(), kind, uint32(id), uint32(imgID)); err != nil {
if err := h.review.DetachImage(c.Request.Context(), kind, uint32(id), uint32(imgID)); err != nil {
renderPage(c, http.StatusInternalServerError, "error", gin.H{"Error": err.Error()})
return
}
@ -571,9 +572,9 @@ func (h *BackstageHandler) IngestJobRetry(c *gin.Context) {
c.Redirect(http.StatusFound, "/admin/ingest-jobs")
}
// draftFormFields 从表单收集可编辑草稿字段(runway + street 一并收集,
// reviewFormFields 从表单收集可编辑字段(runway + street 一并收集,
// 最终白名单在仓储层按 kind 强制,非本 kind 的字段会被忽略)。
func draftFormFields(c *gin.Context) map[string]any {
func reviewFormFields(c *gin.Context) map[string]any {
f := map[string]any{}
if v := c.PostForm("title_en"); v != "" {
f["title_en"] = v

View File

@ -74,7 +74,7 @@ func TestBackstageTemplatesRender(t *testing.T) {
"UID": "abc123", "NameEn": "Acne", "NameCn": "", "Account": "",
"Images": []any{},
// 列表数据
"Items": []any{}, "Users": []any{}, "Kinds": []any{}, "Drafts": []any{},
"Items": []any{}, "Users": []any{}, "Kinds": []any{}, "Records": []any{},
// 详情页实体(.Snap 在两个页面里类型不同,见下面的 perPage 覆盖)
"Article": &dto.PublicArticleDetail{UID: "abc123"},
"Snap": &dto.PublicStreetSnapDetail{UID: "abc123"},
@ -100,7 +100,7 @@ func TestBackstageTemplatesRender(t *testing.T) {
for k, v := range perPage[page] {
data[k] = v
}
data["Draft"] = &service.DraftDetailView{Kind: kind, ID: 7, Title: "T", Status: "pending"}
data["Record"] = &service.RecordDetailView{Kind: kind, ID: 7, Title: "T", Status: "pending"}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

View File

@ -1,37 +1,37 @@
{{define "title"}}草稿审核 · 管理后台{{end}}
{{define "barLabel"}}管理后台 · 草稿审核 #{{.Draft.ID}}{{end}}
{{define "barRight"}}<a href="{{reviewDetailURL .Draft.Kind .Draft.ID}}">返回列表</a> · <a href="/admin/logout">退出</a>{{end}}
{{define "barLabel"}}管理后台 · 草稿审核 #{{.Record.ID}}{{end}}
{{define "barRight"}}<a href="{{reviewDetailURL .Record.Kind .Record.ID}}">返回列表</a> · <a href="/admin/logout">退出</a>{{end}}
{{define "content"}}
{{template "bar" .}}
<div class="main mid">
<p class="muted">类型 <b>{{.Draft.Kind}}</b>{{if .BrandName}} · 品牌 {{.BrandName}}{{end}} · 状态 <b>{{.Draft.Status}}</b></p>
<p class="muted">类型 <b>{{.Record.Kind}}</b>{{if .BrandName}} · 品牌 {{.BrandName}}{{end}} · 状态 <b>{{.Record.Status}}</b></p>
{{/* 字段微调 + 通过:仍是普通表单提交(无 JS 也能用);键盘 a 会触发同一个表单 */}}
<form method="post" action="/admin/reviews/{{.Draft.Kind}}/{{.Draft.ID}}/approve">
{{range .Draft.Fields}}<div><label>{{.Label}}</label>{{if eq .Type "textarea"}}<textarea name="{{.Name}}">{{.Value}}</textarea>{{else}}<input name="{{.Name}}" value="{{.Value}}"{{if eq .Type "number"}} inputmode="numeric"{{end}}>{{end}}</div>{{end}}
<form method="post" action="/admin/reviews/{{.Record.Kind}}/{{.Record.ID}}/approve">
{{range .Record.Fields}}<div><label>{{.Label}}</label>{{if eq .Type "textarea"}}<textarea name="{{.Name}}">{{.Value}}</textarea>{{else}}<input name="{{.Name}}" value="{{.Value}}"{{if eq .Type "number"}} inputmode="numeric"{{end}}>{{end}}</div>{{end}}
<button type="submit">通过并发布</button>
</form>
{{if eq .Draft.Kind "street"}}
{{if eq .Record.Kind "street"}}
<div class="hint">
操作:勾选属于同一个人的图片 → 在<b>其中一张</b>上点「主图」→ 点底部「合并选中为一组」。副图上的「拆出」可恢复为独立主图。<br>
快捷键:<b>j</b>/<b>k</b> 上下移动 · <b>x</b> 勾选 · <b>m</b> 设为主图 · <b>d</b> 拆出 · <b>Enter</b> 看大图 · <b>a</b> 通过
<button type="button" class="x" id="hint-dismiss" aria-label="收起提示">✕</button>
</div>
<h2>图片(<span id="img-stat">{{len .Draft.Images}} 张 · {{len .Draft.Groups}} 组</span>)</h2>
<h2>图片(<span id="img-stat">{{len .Record.Images}} 张 · {{len .Record.Groups}} 组</span>)</h2>
{{/* 合并表单本体在 grid 之前;各格的 radio/checkbox 通过 form="batch" 关联过来 */}}
<form id="batch" method="post" action="/admin/reviews/{{.Draft.Kind}}/{{.Draft.ID}}/images/attach"></form>
<form id="batch" method="post" action="/admin/reviews/{{.Record.Kind}}/{{.Record.ID}}/images/attach"></form>
<div class="grid">
{{range .Draft.Images}}<div class="cell{{if eq .IsDetail 1}} det{{end}}" data-img-id="{{.ID}}">
{{range .Record.Images}}<div class="cell{{if eq .IsDetail 1}} det{{end}}" data-img-id="{{.ID}}">
<button class="thumb" type="button" data-zoom><img src="{{.Image}}" loading="lazy"></button>
<span class="role">{{if eq .IsDetail 1}}副图 → #{{.ParentImageID}}{{else}}主图{{if .DetailCount}} · {{.DetailCount}} 副图{{end}}{{end}}</span>
<span class="nm">{{.Name}}</span>
<div class="acts">
{{if eq .IsDetail 0}}<label class="chk" data-only="main"><input type="radio" form="batch" name="main" value="{{.ID}}"> 主图</label>{{end}}
<label class="chk"><input type="checkbox" form="batch" name="img" value="{{.ID}}"> 选入</label>
{{if eq .IsDetail 1}}<form data-only="detail" method="post" action="/admin/reviews/{{$.Draft.Kind}}/{{$.Draft.ID}}/images/{{.ID}}/detach"><button type="submit">拆出</button></form>{{end}}
<form method="post" action="/admin/reviews/{{$.Draft.Kind}}/{{$.Draft.ID}}/images/{{.ID}}/delete"><button type="submit" class="reject" onclick="return confirm('确认删除这张图片?')">删除</button></form>
{{if eq .IsDetail 1}}<form data-only="detail" method="post" action="/admin/reviews/{{$.Record.Kind}}/{{$.Record.ID}}/images/{{.ID}}/detach"><button type="submit">拆出</button></form>{{end}}
<form method="post" action="/admin/reviews/{{$.Record.Kind}}/{{$.Record.ID}}/images/{{.ID}}/delete"><button type="submit" class="reject" onclick="return confirm('确认删除这张图片?')">删除</button></form>
</div>
</div>{{end}}
</div>
@ -41,13 +41,13 @@
<span class="muted">勾选同一人的图,并在其中一张主图上点「主图」</span>
</div>
{{else}}
<h2>图片管理({{len .Draft.Images}} 张)</h2>
<h2>图片管理({{len .Record.Images}} 张)</h2>
<div class="grid">
{{range .Draft.Images}}<div class="cell" data-img-id="{{.ID}}">
{{range .Record.Images}}<div class="cell" data-img-id="{{.ID}}">
<button class="thumb" type="button" data-zoom><img src="{{.Image}}" loading="lazy"></button>
<span class="nm">{{.Name}}</span>
<div class="acts">
<form method="post" action="/admin/reviews/{{$.Draft.Kind}}/{{$.Draft.ID}}/images/{{.ID}}/delete"><button type="submit" class="reject" onclick="return confirm('确认删除这张图片?')">删除</button></form>
<form method="post" action="/admin/reviews/{{$.Record.Kind}}/{{$.Record.ID}}/images/{{.ID}}/delete"><button type="submit" class="reject" onclick="return confirm('确认删除这张图片?')">删除</button></form>
</div>
</div>{{end}}
</div>

View File

@ -12,7 +12,7 @@
<div class="tabs">
<a href="/admin/reviews{{.ListBase}}" class="{{if eq .Status ""}}on{{end}}">全部</a>
<a href="/admin/reviews{{.ListBase}}?status=pending" class="{{if eq .Status "pending"}}on{{end}}">待审核</a>
<a href="/admin/reviews{{.ListBase}}?status=approved" class="{{if eq .Status "approved"}}on{{end}}">已通过</a>
<a href="/admin/reviews{{.ListBase}}?status=published" class="{{if eq .Status "published"}}on{{end}}">已通过</a>
<a href="/admin/reviews{{.ListBase}}?status=rejected" class="{{if eq .Status "rejected"}}on{{end}}">已驳回</a>
</div>
@ -24,7 +24,7 @@
<th>ID</th><th>类型</th><th>标题</th><th>副标题</th><th>图数</th><th>状态</th><th>操作</th>
</tr></thead>
<tbody>
{{range .Drafts}}<tr data-kind="{{.Kind}}" data-id="{{.ID}}">
{{range .Records}}<tr data-kind="{{.Kind}}" data-id="{{.ID}}">
<td><input type="checkbox" class="row-pick" aria-label="选择这条"></td>
<td><code>{{.ID}}</code></td>
<td>{{if eq .Kind "street"}}<span class="kind street">{{.Kind}}</span>{{else}}<span class="kind">{{.Kind}}</span>{{end}}</td>
@ -32,7 +32,7 @@
<td class="muted">{{.Subtitle}}</td>
<td>{{.ImageCount}}</td>
<td><span class="badge {{.Status}}">{{.Status}}</span></td>
<td><a class="view" href="/admin/reviews/{{.Kind}}/{{.ID}}">查看/审核</a>{{if eq .Status "pending"}} · <button type="button" class="btn" data-quick-approve title="按当前内容直接晋升到正式表">通过</button>{{end}}</td>
<td><a class="view" href="/admin/reviews/{{.Kind}}/{{.ID}}">查看/审核</a>{{if eq .Status "pending"}} · <button type="button" class="btn" data-quick-approve title="按当前内容直接发布">通过</button>{{end}}</td>
</tr>{{end}}
</tbody>
</table>

View File

@ -29,9 +29,9 @@
<a href="/admin/runways">清除</a>
</form>
<table>
<thead><tr><th>编码 ID</th><th>品牌</th><th>标题</th><th>系列</th><th>季节</th><th>年份</th><th>季节码</th><th>图数</th><th></th></tr></thead>
<thead><tr><th>编码 ID</th><th>品牌</th><th>标题</th><th>系列</th><th>季节</th><th>年份</th><th>季节码</th><th>图数</th><th>状态</th><th></th></tr></thead>
<tbody>
{{range .Items}}<tr><td><code>{{.UID}}</code></td><td>{{.BrandName}}</td><td>{{.Title}}</td><td>{{.CollectionType}}</td><td>{{.Season}}</td><td>{{.Year}}</td><td>{{.SeasonCode}}</td><td>{{.ImageCount}}</td><td><a class="view" href="/admin/runways/{{.UID}}">查看</a></td></tr>{{end}}
{{range .Items}}<tr><td><code>{{.UID}}</code></td><td>{{.BrandName}}</td><td>{{.Title}}</td><td>{{.CollectionType}}</td><td>{{.Season}}</td><td>{{.Year}}</td><td>{{.SeasonCode}}</td><td>{{.ImageCount}}</td><td><span class="badge {{.Status}}">{{.Status}}</span></td><td><a class="view" href="/admin/runways/{{.UID}}">查看</a></td></tr>{{end}}
</tbody>
</table>
{{template "pager" .Pager}}

View File

@ -17,9 +17,9 @@
<a href="/admin/street-snaps">清除</a>
</form>
<table>
<thead><tr><th>编码 ID</th><th>标题</th><th>图片数</th><th></th></tr></thead>
<thead><tr><th>编码 ID</th><th>标题</th><th>图片数</th><th>状态</th><th></th></tr></thead>
<tbody>
{{range .Items}}<tr><td><code>{{.UID}}</code></td><td>{{.Title}}</td><td>{{.ImageCount}}</td><td><a class="view" href="/admin/street-snaps/{{.UID}}">查看</a></td></tr>{{end}}
{{range .Items}}<tr><td><code>{{.UID}}</code></td><td>{{.Title}}</td><td>{{.ImageCount}}</td><td><span class="badge {{.Status}}">{{.Status}}</span></td><td><a class="view" href="/admin/street-snaps/{{.UID}}">查看</a></td></tr>{{end}}
</tbody>
</table>
{{template "pager" .Pager}}