This commit is contained in:
toom1996
2026-09-28 10:52:50 +08:00
parent aaeab41d67
commit d15d2a4701
42 changed files with 19884 additions and 337 deletions

View File

@ -501,3 +501,22 @@ tbody tr:last-child td { border-bottom: 0; }
.row { flex-direction: column; gap: 0; }
.grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
}
/* ---------- 编辑页重复图标记 ---------- */
.cell .badge.dup { margin-left: 8px; vertical-align: middle; }
.cell .dup-src { margin: 0 8px 6px; font-size: 12px; }
.cell .dup-src > summary { cursor: pointer; color: #6b21a8; user-select: none; outline: none; }
.cell .dup-src-body {
display: flex;
gap: 8px;
align-items: center;
margin-top: 6px;
padding: 6px;
background: #faf5ff;
border: 1px solid #e9d5ff;
border-radius: 6px;
}
.cell .dup-src-body img { width: 56px; height: 84px; object-fit: cover; border-radius: 4px; flex: 0 0 auto; background: #eee; }
.cell .dup-title { margin-bottom: 2px; color: #333; }
.cell .dup-link { color: #6b21a8; font-weight: 600; text-decoration: none; }
.cell .dup-link:hover { text-decoration: underline; }

View File

@ -1,6 +1,6 @@
// 后台交互脚本(原生 JS,无构建步骤)。data-admin-js
// 两个入口页面:
// - 文章管理混合列表(/admin/articles):批量通过/驳回
// - 文章管理混合列表(/admin/articles):批量通过
// - 街拍编辑页(/admin/street-snaps/:id/edit):主副图合并/拆出(街拍分组)
(function () {
@ -25,7 +25,7 @@
});
}
// ── 文章管理列表:批量通过 / 驳回 ──────────────────────────────────────
// ── 文章管理列表:批量通过 ──────────────────────────────────────
function initArticleList() {
var selAll = document.getElementById('sel-all');
var picks = function () { return Array.prototype.slice.call(document.querySelectorAll('.row-pick')); };
@ -58,7 +58,7 @@
return { kind: tr.getAttribute('data-kind'), id: parseInt(tr.getAttribute('data-id'), 10) };
});
if (items.length === 0) { alert('请先勾选要操作的记录'); return; }
if (!confirm('确认' + (action === 'approve' ? '通过' : '驳回') + '选中的 ' + items.length + ' 条?')) return;
if (!confirm('确认通过选中的 ' + items.length + ' 条?')) return;
fetch('/admin/api/articles/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -71,9 +71,7 @@
}).catch(function (e) { alert('请求失败:' + e); });
}
var approve = document.getElementById('batch-approve');
var reject = document.getElementById('batch-reject');
if (approve) approve.addEventListener('click', function () { postBatch('approve'); });
if (reject) reject.addEventListener('click', function () { postBatch('reject'); });
}
// ── 街拍编辑页:主副图分组(合并 / 拆出)──────────────────────────────

View File

@ -3,7 +3,7 @@
// 为什么需要它们:审核是图片密集型工作,原先前端只有「表单 POST → 302 → 整页刷新」一条路,
// 于是批量通过 20 条要点 20 次、每次刷新还会丢掉滚动位置与筛选状态。
// 这些接口让页面能用一小段 JS 做「多选一次性提交 / 局部更新」,
// 同时表单式路由(/admin/articles/... 下的 approve/reject/attach/detach)原样保留:关掉 JS 后台依然可用。
// 同时表单式路由(/admin/articles/... 下的 approve/attach/detach)原样保留:关掉 JS 后台依然可用。
//
// 所有接口只做参数校验与组装,业务复用 ReviewService,不新增业务逻辑分支。
package handler
@ -26,13 +26,12 @@ type reviewBatchItem struct {
}
type reviewBatchReq struct {
// Action: approve | reject
// Action: approve
Action string `json:"action"`
Reason string `json:"reason"`
Items []reviewBatchItem `json:"items"`
}
// ArticleBatch 文章管理批量通过 / 驳回(替代原审核批量接口)。
// ArticleBatch 文章管理批量通过(替代原审核批量接口)。
//
// 请求体:{"action":"approve","items":[{"kind":"street","id":12}, ...]}
// 响应: {"ok":2,"failed":[{"kind":"street","id":9,"error":"..."}],"pending":17}
@ -44,8 +43,8 @@ func (h *BackstageHandler) ArticleBatch(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误"})
return
}
if req.Action != "approve" && req.Action != "reject" {
c.JSON(http.StatusBadRequest, gin.H{"error": "action 只能是 approve 或 reject"})
if req.Action != "approve" {
c.JSON(http.StatusBadRequest, gin.H{"error": "action 只能是 approve"})
return
}
if len(req.Items) == 0 {
@ -56,11 +55,6 @@ func (h *BackstageHandler) ArticleBatch(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "一次最多处理 200 条"})
return
}
if req.Action == "reject" && req.Reason == "" {
// 驳回理由必填:留痕是审核流程的意义所在。
c.JSON(http.StatusBadRequest, gin.H{"error": "驳回必须填写理由"})
return
}
ctx := c.Request.Context()
registered := h.registeredKinds(ctx)
@ -77,8 +71,6 @@ func (h *BackstageHandler) ArticleBatch(c *gin.Context) {
var err error
if req.Action == "approve" {
_, err = h.review.Approve(ctx, item.Kind, item.ID, reviewer)
} else {
err = h.review.Reject(ctx, item.Kind, item.ID, reviewer, req.Reason)
}
if err != nil {
failed = append(failed, gin.H{"kind": item.Kind, "id": item.ID, "error": err.Error()})

View File

@ -311,7 +311,7 @@ func (h *BackstageHandler) StreetSnapDetail(c *gin.Context) {
// ── 文章管理(走秀 + 街拍混合列表,替代原审核页与分类型列表)─────────────
// Articles 文章管理混合列表:跨 brand_runways / street_snaps 统一展示,按状态 + 类型筛选。
// 这是「审核 / 走秀 / 街拍」三页合一后的唯一入口:通过 / 驳回 / 编辑 / 下架都在这里发起。
// 这是「审核 / 走秀 / 街拍」三页合一后的唯一入口:通过 / 编辑 / 删除(下架)都在这里发起。
func (h *BackstageHandler) Articles(c *gin.Context) {
status := c.Query("status")
kind := c.Query("kind")
@ -354,7 +354,7 @@ func (h *BackstageHandler) Articles(c *gin.Context) {
}
// 状态 tab 角标(空串=全部)。
tabCounts := map[string]int64{}
for _, s := range []string{"", "pending", "published", "rejected"} {
for _, s := range []string{"", "pending", "published"} {
n, e := h.adminRepo.CountMixed(c.Request.Context(), s, kind)
if e == nil {
tabCounts[s] = n
@ -424,23 +424,6 @@ func (h *BackstageHandler) ArticleApprove(c *gin.Context) {
c.Redirect(http.StatusFound, "/admin/articles?status=pending")
}
// ArticleReject 驳回:记录状态与理由(理由用于审核留痕)。
func (h *BackstageHandler) ArticleReject(c *gin.Context) {
kind := c.Param("kind")
id := uint32(atoiDefault(c.Param("id"), 0))
if id == 0 {
renderPage(c, http.StatusBadRequest, "error", gin.H{"Error": "参数无效"})
return
}
reason := c.PostForm("reason")
username, _ := c.Get(middleware.ContextUsername)
if err := h.review.Reject(c.Request.Context(), kind, id, usernameString(username), reason); err != nil {
renderPage(c, http.StatusInternalServerError, "error", gin.H{"Error": err.Error()})
return
}
c.Redirect(http.StatusFound, "/admin/articles?status=pending")
}
// ArticleAttachImages 把勾选的图并入指定主图(街拍「合并选中为一组」的表单版;AJAX 版见 ArticleAttachImagesAPI)。
func (h *BackstageHandler) ArticleAttachImages(c *gin.Context) {
kind := c.Param("kind")
@ -655,21 +638,25 @@ func (h *BackstageHandler) RunwayEdit(c *gin.Context) {
for _, m := range mains {
mainIDs[m.ID] = true
cells = append(cells, gin.H{
"id": m.ID,
"image": m.Image,
"name": m.Name,
"isDetail": 0,
"role": "主图",
"DetCount": len(detailsByParent[m.ID]),
"id": m.ID,
"image": m.Image,
"name": m.Name,
"isDetail": 0,
"role": "主图",
"DetCount": len(detailsByParent[m.ID]),
"isDuplicate": int(m.IsDuplicate),
"dupOf": m.DupOf,
})
for _, d := range detailsByParent[m.ID] {
cells = append(cells, gin.H{
"id": d.ID,
"image": d.Image,
"name": d.Name,
"isDetail": 1,
"role": fmt.Sprintf("细节图 → #%d", d.ParentImageID),
"parent": d.ParentImageID,
"id": d.ID,
"image": d.Image,
"name": d.Name,
"isDetail": 1,
"role": fmt.Sprintf("细节图 → #%d", d.ParentImageID),
"parent": d.ParentImageID,
"isDuplicate": int(d.IsDuplicate),
"dupOf": d.DupOf,
})
}
}
@ -680,13 +667,15 @@ func (h *BackstageHandler) RunwayEdit(c *gin.Context) {
}
for _, d := range ds {
cells = append(cells, gin.H{
"id": d.ID,
"image": d.Image,
"name": d.Name,
"isDetail": 1,
"role": fmt.Sprintf("细节图 → #%d", d.ParentImageID),
"parent": d.ParentImageID,
"orphan": true,
"id": d.ID,
"image": d.Image,
"name": d.Name,
"isDetail": 1,
"role": fmt.Sprintf("细节图 → #%d", d.ParentImageID),
"parent": d.ParentImageID,
"orphan": true,
"isDuplicate": int(d.IsDuplicate),
"dupOf": d.DupOf,
})
}
}
@ -694,18 +683,20 @@ func (h *BackstageHandler) RunwayEdit(c *gin.Context) {
if n := len(imgs); n > 0 {
dupPct = float64(dupCount) / float64(n) * 100
}
dupSources := h.buildDupSources(c.Request.Context(), "runway", cells)
renderPage(c, http.StatusOK, "runway-edit", gin.H{
"UID": raw,
"Kind": "runway",
"RecordID": rw.ID,
"Status": rw.Status,
"Runway": rw,
"Images": imgs,
"Cells": cells,
"BrandName": brandName,
"IsDown": rw.IsDeleted == 1,
"DupCount": dupCount,
"DupPct": dupPct,
"UID": raw,
"Kind": "runway",
"RecordID": rw.ID,
"Status": rw.Status,
"Runway": rw,
"Images": imgs,
"Cells": cells,
"BrandName": brandName,
"IsDown": rw.IsDeleted == 1,
"DupCount": dupCount,
"DupPct": dupPct,
"DupSources": dupSources,
})
}
@ -832,21 +823,25 @@ func (h *BackstageHandler) StreetSnapEdit(c *gin.Context) {
for _, m := range mains {
mainIDs[m.ID] = true
cells = append(cells, gin.H{
"id": m.ID,
"image": m.Image,
"name": m.Name,
"isDetail": 0,
"role": "主图",
"DetCount": len(detailsByParent[m.ID]),
"id": m.ID,
"image": m.Image,
"name": m.Name,
"isDetail": 0,
"role": "主图",
"DetCount": len(detailsByParent[m.ID]),
"isDuplicate": int(m.IsDuplicate),
"dupOf": m.DupOf,
})
for _, d := range detailsByParent[m.ID] {
cells = append(cells, gin.H{
"id": d.ID,
"image": d.Image,
"name": d.Name,
"isDetail": 1,
"role": fmt.Sprintf("副图 → #%d", d.ParentImageID),
"parent": d.ParentImageID,
"id": d.ID,
"image": d.Image,
"name": d.Name,
"isDetail": 1,
"role": fmt.Sprintf("副图 → #%d", d.ParentImageID),
"parent": d.ParentImageID,
"isDuplicate": int(d.IsDuplicate),
"dupOf": d.DupOf,
})
}
}
@ -857,13 +852,15 @@ func (h *BackstageHandler) StreetSnapEdit(c *gin.Context) {
}
for _, d := range ds {
cells = append(cells, gin.H{
"id": d.ID,
"image": d.Image,
"name": d.Name,
"isDetail": 1,
"role": fmt.Sprintf("副图 → #%d", d.ParentImageID),
"parent": d.ParentImageID,
"orphan": true,
"id": d.ID,
"image": d.Image,
"name": d.Name,
"isDetail": 1,
"role": fmt.Sprintf("副图 → #%d", d.ParentImageID),
"parent": d.ParentImageID,
"orphan": true,
"isDuplicate": int(d.IsDuplicate),
"dupOf": d.DupOf,
})
}
}
@ -871,20 +868,78 @@ func (h *BackstageHandler) StreetSnapEdit(c *gin.Context) {
if n := len(imgs); n > 0 {
dupPct = float64(dupCount) / float64(n) * 100
}
dupSources := h.buildDupSources(c.Request.Context(), "street", cells)
renderPage(c, http.StatusOK, "snap-edit", gin.H{
"UID": raw,
"Kind": "street",
"RecordID": snap.ID,
"Status": snap.Status,
"Snap": snap,
"Images": imgs,
"Cells": cells,
"IsDown": snap.IsDeleted == 1,
"DupCount": dupCount,
"DupPct": dupPct,
"UID": raw,
"Kind": "street",
"RecordID": snap.ID,
"Status": snap.Status,
"Snap": snap,
"Images": imgs,
"Cells": cells,
"IsDown": snap.IsDeleted == 1,
"DupCount": dupCount,
"DupPct": dupPct,
"DupSources": dupSources,
})
}
// buildDupSources 反查重复源上下文,返回以源图 id(dup_of 字符串)为键的模板数据。
// 仅对有重复标记(isDuplicate==1 且 dup_of 非空)的图做批量反查,避免逐图 N+1。
func (h *BackstageHandler) buildDupSources(ctx context.Context, kind string, cells []gin.H) map[string]gin.H {
out := map[string]gin.H{}
seen := map[uint32]bool{}
ids := make([]uint32, 0)
for _, c := range cells {
if v, ok := c["isDuplicate"].(int); !ok || v != 1 {
continue
}
s, ok := c["dupOf"].(string)
if !ok || s == "" {
continue
}
id, err := strconv.ParseUint(s, 10, 32)
if err != nil {
continue
}
u := uint32(id)
if seen[u] {
continue
}
seen[u] = true
ids = append(ids, u)
}
if len(ids) == 0 {
return out
}
var (
srcs map[uint32]dto.DupSource
err error
)
if kind == "street" {
srcs, err = h.street.GetStreetSnapDupSources(ctx, ids)
} else {
srcs, err = h.article.GetRunwayDupSources(ctx, ids)
}
if err != nil {
return out
}
typ := byte(hashid.TypeRunway)
if kind == "street" {
typ = byte(hashid.TypeSnap)
}
for id, s := range srcs {
out[strconv.FormatUint(uint64(id), 10)] = gin.H{
"srcImage": s.Image,
"srcTitle": s.Title,
"srcBrand": s.Brand,
"srcUID": hashid.EncodeWithType(s.RecordID, typ),
"srcKind": kind,
}
}
return out
}
// StreetSnapUpdate 应用街拍字段编辑。
func (h *BackstageHandler) StreetSnapUpdate(c *gin.Context) {
raw := c.Param("id")

View File

@ -5,7 +5,7 @@
{{template "bar" .}}
<div class="main">
<h1>文章管理</h1>
<p class="muted">走秀与街拍合并管理:通过 / 驳回 / 编辑 / 下架都在此处发起。待审核优先排在前面。</p>
<p class="muted">走秀与街拍合并管理:通过 / 编辑 / 删除(下架,清空 S4)都在此处发起。待审核优先排在前面。</p>
<div class="kinds">
<a href="/admin/articles?status={{.Status}}" class="{{if eq .Kind ""}}on{{end}}">全部类型</a>
<a href="/admin/articles?status={{.Status}}&kind=runway" class="{{if eq .Kind "runway"}}on{{end}}">走秀</a>
@ -15,7 +15,6 @@
<a href="/admin/articles?status=&kind={{.Kind}}" class="{{if eq .Status ""}}on{{end}}">全部{{if gt (index .TabCounts "") 0}}({{index .TabCounts ""}}){{end}}</a>
<a href="/admin/articles?status=pending&kind={{.Kind}}" class="{{if eq .Status "pending"}}on{{end}}">待审核{{if gt (index .TabCounts "pending") 0}}({{index .TabCounts "pending"}}){{end}}</a>
<a href="/admin/articles?status=published&kind={{.Kind}}" class="{{if eq .Status "published"}}on{{end}}">已发布{{if gt (index .TabCounts "published") 0}}({{index .TabCounts "published"}}){{end}}</a>
<a href="/admin/articles?status=rejected&kind={{.Kind}}" class="{{if eq .Status "rejected"}}on{{end}}">已驳回{{if gt (index .TabCounts "rejected") 0}}({{index .TabCounts "rejected"}}){{end}}</a>
</div>
<div class="table-wrap">
@ -38,8 +37,7 @@
<div class="ops">
{{if eq .Kind "runway"}}<a class="view" href="/admin/runways/{{.UID}}/edit">编辑</a>{{else}}<a class="view" href="/admin/street-snaps/{{.UID}}/edit">编辑</a>{{end}}
{{if eq .Status "pending"}}<form class="inline" method="post" action="/admin/articles/{{.Kind}}/{{.ID}}/approve"><button type="submit" class="op op-primary">通过</button></form>{{end}}
{{if eq .Status "pending"}}<form class="inline" method="post" action="/admin/articles/{{.Kind}}/{{.ID}}/reject"><button type="submit" class="op op-danger">驳回</button></form>{{end}}
{{if eq .Kind "runway"}}<form class="inline" method="post" action="/admin/runways/{{.UID}}/toggle"><input type="hidden" name="deleted" value="1"><button type="submit" class="op op-warn" onclick="return confirm('确认下架(对外不可见)?')">下架</button></form>{{else}}<form class="inline" method="post" action="/admin/street-snaps/{{.UID}}/toggle"><input type="hidden" name="deleted" value="1"><button type="submit" class="op op-warn" onclick="return confirm('确认下架(对外不可见)?')">下架</button></form>{{end}}
{{if eq .Kind "runway"}}<form class="inline" method="post" action="/admin/runways/{{.UID}}/toggle"><input type="hidden" name="deleted" value="1"><button type="submit" class="op op-warn" onclick="return confirm('确认删除(清空 S4)?')">删除</button></form>{{else}}<form class="inline" method="post" action="/admin/street-snaps/{{.UID}}/toggle"><input type="hidden" name="deleted" value="1"><button type="submit" class="op op-warn" onclick="return confirm('确认删除(清空 S4)?')">删除</button></form>{{end}}
</div>
</td>
</tr>{{end}}
@ -52,7 +50,6 @@
<span class="muted" id="sel-count">已选 0 条</span>
<span class="spacer"></span>
<button type="button" id="batch-approve">批量通过</button>
<button type="button" id="batch-reject" class="reject">批量驳回…</button>
<button type="button" id="batch-clear" class="ghost">清空选择</button>
</div>
</div>

View File

@ -8,7 +8,7 @@
{{if .Kinds}}<div class="chips">{{range .Kinds}}{{if ne .Kind ""}}<span class="chip">{{.Label}}:待审 {{.Pending}}</span>{{end}}{{end}}</div>{{end}}
<div class="cards">
<a class="card" href="/admin/brands"><div class="t">品牌管理</div><div class="d">查看 / 搜索全部品牌</div></a>
<a class="card" href="/admin/articles"><div class="t">文章管理{{if gt .PendingReviews 0}}(待审 {{.PendingReviews}}){{end}}</div><div class="d">走秀 / 街拍:通过 · 驳回 · 编辑 · 下架</div></a>
<a class="card" href="/admin/articles"><div class="t">文章管理{{if gt .PendingReviews 0}}(待审 {{.PendingReviews}}){{end}}</div><div class="d">走秀 / 街拍:通过 · 编辑 · 删除(下架)</div></a>
<a class="card" href="/admin/ingest-jobs"><div class="t">入库任务</div><div class="d">爬虫上报处理进度 / 失败重试</div></a>
<a class="card" href="/admin/users"><div class="t">用户管理</div><div class="d">账号等级 · 提级 VIP</div></a>
</div>

View File

@ -18,12 +18,11 @@
{{if eq .Runway.Status "pending"}}
<div class="review-actions">
<form method="post" action="/admin/articles/runway/{{.Runway.ID}}/approve"><button type="submit" class="op op-primary">通过并发布</button></form>
<form method="post" action="/admin/articles/runway/{{.Runway.ID}}/reject"><input name="reason" placeholder="驳回理由(可选)"><button type="submit" class="op op-danger">驳回</button></form>
</div>
{{end}}
<form method="post" action="/admin/runways/{{.UID}}/toggle" style="margin-top:22px">
{{if .IsDown}}<input type="hidden" name="deleted" value="0"><button type="submit">重新上架</button>{{else}}<input type="hidden" name="deleted" value="1"><button type="submit" class="reject">下架(对外不可见)</button>{{end}}
{{if .IsDown}}<input type="hidden" name="deleted" value="0"><button type="submit">重新上架</button>{{else}}<input type="hidden" name="deleted" value="1"><button type="submit" class="reject">删除(清空 S4)</button>{{end}}
</form>
<p class="muted" style="margin-top:10px">下架会级联软删全部图片,并异步把S4孤儿图清理任务入队(按引用计数判定,共享图不误删)。</p>
</div>

View File

@ -16,12 +16,11 @@
{{if eq .Snap.Status "pending"}}
<div class="review-actions">
<form method="post" action="/admin/articles/street/{{.Snap.ID}}/approve"><button type="submit" class="op op-primary">通过并发布</button></form>
<form method="post" action="/admin/articles/street/{{.Snap.ID}}/reject"><input name="reason" placeholder="驳回理由(可选)"><button type="submit" class="op op-danger">驳回</button></form>
</div>
{{end}}
<form method="post" action="/admin/street-snaps/{{.UID}}/toggle" style="margin-top:22px">
{{if .IsDown}}<input type="hidden" name="deleted" value="0"><button type="submit">重新上架</button>{{else}}<input type="hidden" name="deleted" value="1"><button type="submit" class="reject">下架(对外不可见)</button>{{end}}
{{if .IsDown}}<input type="hidden" name="deleted" value="0"><button type="submit">重新上架</button>{{else}}<input type="hidden" name="deleted" value="1"><button type="submit" class="reject">删除(清空 S4)</button>{{end}}
</form>
<p class="muted" style="margin-top:10px">下架会级联软删全部图片,并异步把S4孤儿图清理任务入队(按引用计数判定,共享图不误删)。</p>
</div>

View File

@ -52,13 +52,16 @@
{{if eq .Kind "runway"}}走秀图按 Look 分组:同一 Look 的主图 + 其细节图(副图)。{{end}}
<button type="button" class="x" id="hint-dismiss" aria-label="收起提示">✕</button>
</div>
{{$kind := .Kind}}{{$uid := .UID}}{{$rid := .RecordID}}
{{$kind := .Kind}}{{$uid := .UID}}{{$rid := .RecordID}}{{$dupSources := .DupSources}}
<form id="batch" method="post" action="/admin/articles/{{$kind}}/{{$rid}}/images/attach"></form>
<div class="grid">
{{range .Cells}}<div class="cell{{if eq .isDetail 1}} det{{if .orphan}} shown{{end}}{{end}}" data-img-id="{{.id}}"{{if eq .isDetail 1}} data-parent="{{.parent}}"{{end}}>
<button class="thumb" type="button" data-zoom><img src="{{imgURL .image}}" loading="lazy"></button>
<span class="role">{{.role}}</span>
<span class="nm">{{.name}}</span>
{{if eq .isDuplicate 1}}<span class="badge dup">重复</span>
{{with index $dupSources .dupOf}}{{if .srcTitle}}<details class="dup-src"><summary>查看重复来源</summary><div class="dup-src-body"><img src="{{imgURL .srcImage}}" loading="lazy" alt="源图缩略图"><div><div class="dup-title">属于:<b>{{.srcTitle}}</b>{{if .srcBrand}} · {{.srcBrand}}{{end}}</div><a class="dup-link" href="/admin/{{if eq .srcKind "runway"}}runways{{else}}street-snaps{{end}}/{{.srcUID}}/edit" target="_blank">打开所属{{if eq .srcKind "runway"}}走秀{{else}}街拍{{end}} →</a></div></div></details>{{end}}{{end}}
{{end}}
<div class="acts">
{{if eq $kind "runway"}}<form method="post" action="/admin/runways/{{$uid}}/cover"><input type="hidden" name="image" value="{{.image}}"><button type="submit">设封面</button></form>
<form method="post" action="/admin/runways/{{$uid}}/images/{{.id}}/delete"><button type="submit" class="reject">删除</button></form>