feat: Admin 媒体库列表、删除与同步索引
SSR /admin/media 复用 ListMedia/DeleteMedia,并支持按 ID 批量删与扫盘回填。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
255
routers/web/admin_media.go
Normal file
255
routers/web/admin_media.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type adminMediaRow struct {
|
||||
ID uint
|
||||
Category string
|
||||
CategoryLabel string
|
||||
Name string
|
||||
URL string
|
||||
ThumbURL string
|
||||
SizeLabel string
|
||||
Modified string
|
||||
StorageType string
|
||||
}
|
||||
|
||||
type adminMediaData struct {
|
||||
AdminChrome
|
||||
Files []adminMediaRow
|
||||
Keyword string
|
||||
Category string
|
||||
Page int
|
||||
Total int
|
||||
HasPrev bool
|
||||
HasMore bool
|
||||
PrevPage int
|
||||
NextPage int
|
||||
QuerySuffix string
|
||||
StorageType string
|
||||
CountAll int
|
||||
CountAvatars int
|
||||
CountPosts int
|
||||
CountSite int
|
||||
}
|
||||
|
||||
// AdminMediaGet 媒体库列表
|
||||
func (d Deps) AdminMediaGet(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
d.renderAdminMedia(ctx, "")
|
||||
}
|
||||
|
||||
func (d Deps) renderAdminMedia(ctx *webctx.Context, errMsg string) {
|
||||
chrome := d.adminChrome(ctx, "媒体", "media")
|
||||
chrome.Error = errMsg
|
||||
page, _ := strconv.Atoi(ctx.C.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
keyword := strings.TrimSpace(ctx.C.Query("q"))
|
||||
category := strings.TrimSpace(ctx.C.DefaultQuery("category", "all"))
|
||||
if category == "" {
|
||||
category = "all"
|
||||
}
|
||||
size := 24
|
||||
data := adminMediaData{
|
||||
AdminChrome: chrome,
|
||||
Keyword: keyword,
|
||||
Category: category,
|
||||
Page: page,
|
||||
PrevPage: page - 1,
|
||||
NextPage: page + 1,
|
||||
HasPrev: page > 1,
|
||||
Files: []adminMediaRow{},
|
||||
}
|
||||
q := url.Values{}
|
||||
if keyword != "" {
|
||||
q.Set("q", keyword)
|
||||
}
|
||||
if category != "" && category != "all" {
|
||||
q.Set("category", category)
|
||||
}
|
||||
data.QuerySuffix = q.Encode()
|
||||
|
||||
if d.Store == nil {
|
||||
chrome.Error = "上传存储未就绪"
|
||||
data.AdminChrome = chrome
|
||||
ctx.HTML(http.StatusOK, "admin/media", data)
|
||||
return
|
||||
}
|
||||
result, err := d.Store.ListMedia(category, keyword, page, size)
|
||||
if err != nil {
|
||||
chrome.Error = err.Error()
|
||||
data.AdminChrome = chrome
|
||||
ctx.HTML(http.StatusOK, "admin/media", data)
|
||||
return
|
||||
}
|
||||
data.Total = result.Total
|
||||
data.Page = result.Page
|
||||
data.HasPrev = result.Page > 1
|
||||
data.HasMore = result.Page < result.TotalPages
|
||||
data.PrevPage = result.Page - 1
|
||||
data.NextPage = result.Page + 1
|
||||
data.StorageType = result.StorageType
|
||||
data.CountAvatars = result.CategoryCounts[services.UploadCategoryAvatars]
|
||||
data.CountPosts = result.CategoryCounts[services.UploadCategoryPosts]
|
||||
data.CountSite = result.CategoryCounts[services.UploadCategorySite]
|
||||
data.CountAll = data.CountAvatars + data.CountPosts + data.CountSite
|
||||
data.Files = make([]adminMediaRow, 0, len(result.Files))
|
||||
for _, f := range result.Files {
|
||||
thumb := services.ThumbURLFromUpload(f.URL)
|
||||
if thumb == "" {
|
||||
thumb = f.URL
|
||||
}
|
||||
data.Files = append(data.Files, adminMediaRow{
|
||||
ID: f.ID, Category: f.Category, CategoryLabel: adminMediaCategoryLabel(f.Category),
|
||||
Name: f.Name, URL: f.URL, ThumbURL: thumb,
|
||||
SizeLabel: formatByteSize(f.Size), Modified: f.ModifiedAt.Local().Format("2006-01-02 15:04"),
|
||||
StorageType: f.StorageType,
|
||||
})
|
||||
}
|
||||
ctx.HTML(http.StatusOK, "admin/media", data)
|
||||
}
|
||||
|
||||
func adminMediaCategoryLabel(cat string) string {
|
||||
switch cat {
|
||||
case services.UploadCategoryAvatars:
|
||||
return "头像"
|
||||
case services.UploadCategoryPosts:
|
||||
return "帖图"
|
||||
case services.UploadCategorySite:
|
||||
return "站点"
|
||||
default:
|
||||
return cat
|
||||
}
|
||||
}
|
||||
|
||||
func formatByteSize(n int64) string {
|
||||
if n < 1024 {
|
||||
return fmt.Sprintf("%d B", n)
|
||||
}
|
||||
if n < 1024*1024 {
|
||||
return fmt.Sprintf("%.1f KB", float64(n)/1024)
|
||||
}
|
||||
return fmt.Sprintf("%.1f MB", float64(n)/(1024*1024))
|
||||
}
|
||||
|
||||
func (d Deps) mediaRedirectQuery(c *gin.Context) string {
|
||||
q := url.Values{}
|
||||
if v := strings.TrimSpace(c.Query("q")); v != "" {
|
||||
q.Set("q", v)
|
||||
}
|
||||
if v := strings.TrimSpace(c.Query("category")); v != "" && v != "all" {
|
||||
q.Set("category", v)
|
||||
}
|
||||
if v := strings.TrimSpace(c.Query("page")); v != "" && v != "1" {
|
||||
q.Set("page", v)
|
||||
}
|
||||
// POST 表单也可带回筛选
|
||||
if v := strings.TrimSpace(c.PostForm("q")); v != "" {
|
||||
q.Set("q", v)
|
||||
}
|
||||
if v := strings.TrimSpace(c.PostForm("category")); v != "" && v != "all" {
|
||||
q.Set("category", v)
|
||||
}
|
||||
if v := strings.TrimSpace(c.PostForm("page")); v != "" && v != "1" {
|
||||
q.Set("page", v)
|
||||
}
|
||||
s := q.Encode()
|
||||
if s == "" {
|
||||
return "/admin/media"
|
||||
}
|
||||
return "/admin/media?" + s
|
||||
}
|
||||
|
||||
// AdminMediaDeletePost 按 ID 删除单条
|
||||
func (d Deps) AdminMediaDeletePost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if !ctx.CheckCSRF() {
|
||||
d.renderAdminMedia(ctx, "无效请求,请重试")
|
||||
return
|
||||
}
|
||||
if d.Store == nil {
|
||||
d.renderAdminMedia(ctx, "上传存储未就绪")
|
||||
return
|
||||
}
|
||||
id64, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
n, err := d.Store.DeleteMediaByIDs([]uint{uint(id64)})
|
||||
if err != nil {
|
||||
d.renderAdminMedia(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
d.renderAdminMedia(ctx, "未找到可删除的媒体")
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("已删除 1 项媒体")
|
||||
ctx.Redirect(d.mediaRedirectQuery(c))
|
||||
}
|
||||
|
||||
// AdminMediaBatchDeletePost 批量删除
|
||||
func (d Deps) AdminMediaBatchDeletePost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if !ctx.CheckCSRF() {
|
||||
d.renderAdminMedia(ctx, "无效请求,请重试")
|
||||
return
|
||||
}
|
||||
if d.Store == nil {
|
||||
d.renderAdminMedia(ctx, "上传存储未就绪")
|
||||
return
|
||||
}
|
||||
raw := c.PostFormArray("ids")
|
||||
ids := make([]uint, 0, len(raw))
|
||||
for _, s := range raw {
|
||||
id64, err := strconv.ParseUint(strings.TrimSpace(s), 10, 64)
|
||||
if err != nil || id64 == 0 {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, uint(id64))
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
d.renderAdminMedia(ctx, "请选择要删除的文件")
|
||||
return
|
||||
}
|
||||
if len(ids) > 100 {
|
||||
d.renderAdminMedia(ctx, "单次最多删除 100 个文件")
|
||||
return
|
||||
}
|
||||
n, err := d.Store.DeleteMediaByIDs(ids)
|
||||
if err != nil {
|
||||
d.renderAdminMedia(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.SetFlash(fmt.Sprintf("已删除 %d 项媒体", n))
|
||||
ctx.Redirect(d.mediaRedirectQuery(c))
|
||||
}
|
||||
|
||||
// AdminMediaSyncPost 扫盘回填索引
|
||||
func (d Deps) AdminMediaSyncPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if !ctx.CheckCSRF() {
|
||||
d.renderAdminMedia(ctx, "无效请求,请重试")
|
||||
return
|
||||
}
|
||||
if d.Store == nil {
|
||||
d.renderAdminMedia(ctx, "上传存储未就绪")
|
||||
return
|
||||
}
|
||||
n, err := d.Store.SyncMediaIndex()
|
||||
if err != nil {
|
||||
d.renderAdminMedia(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.SetFlash(fmt.Sprintf("索引已同步(处理 %d 条)", n))
|
||||
ctx.Redirect("/admin/media")
|
||||
}
|
||||
@@ -108,6 +108,10 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
admin.GET("/badges/:id/edit", deps.AdminBadgeEditGet)
|
||||
admin.POST("/badges/:id", deps.AdminBadgeUpdate)
|
||||
admin.POST("/badges/:id/delete", deps.AdminBadgeDelete)
|
||||
admin.GET("/media", deps.AdminMediaGet)
|
||||
admin.POST("/media/delete", deps.AdminMediaBatchDeletePost)
|
||||
admin.POST("/media/sync", deps.AdminMediaSyncPost)
|
||||
admin.POST("/media/:id/delete", deps.AdminMediaDeletePost)
|
||||
admin.GET("/trash", deps.AdminTrashGet)
|
||||
admin.POST("/trash/:id/restore", deps.AdminTrashRestorePost)
|
||||
admin.POST("/trash/:id/purge", deps.AdminTrashPurgePost)
|
||||
|
||||
Reference in New Issue
Block a user