feat: 增加 Web 管理界面,扫描整理改为页面点击触发

- main.go 改为 Web 服务入口(Gin + 内嵌前端 dist + 自动打开浏览器)
- internal/service: 新增扫描整理后台任务,支持状态机与实时日志捕获
- internal/api: 新增统一响应、媒体列表/统计、扫描开始/状态接口
- internal/store: 追加 ListMedia/CountMedia/Stats 查询,MediaInfo 补 JSON tag
- internal/config: application.properties 配置加载(端口/数据库/自动开浏览器)
- frontend: Vue2 + ElementUI + Vite 工程,含扫描整理/统计看板/媒体库三页
- build.sh 整合前端构建,输出单一 kenaito-media-picker.exe
This commit is contained in:
2025-04-21 21:08:02 +08:00
parent 767b093b45
commit e053b378fb
29 changed files with 3684 additions and 104 deletions
+124
View File
@@ -0,0 +1,124 @@
package api
import (
"fmt"
"net/http"
"github.com/dustin/go-humanize"
"github.com/gin-gonic/gin"
"kenaito-media-picker/internal/classify"
"kenaito-media-picker/internal/store"
)
// MediaController 媒体库接口
type MediaController struct {
store *store.Store
}
// NewMediaController 创建媒体控制器,注入数据访问层
func NewMediaController(st *store.Store) *MediaController {
return &MediaController{store: st}
}
// mediaListReq 列表查询请求
type mediaListReq struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
MediaType string `json:"mediaType"`
Orientation string `json:"orientation"`
Keyword string `json:"keyword"`
Codec string `json:"codec"`
Format string `json:"format"`
Resolution string `json:"resolution"`
SortField string `json:"sortField"`
SortOrder string `json:"sortOrder"`
}
// mediaView 列表展示视图:补充方向/分辨率/可读大小等派生字段
type mediaView struct {
store.MediaInfo
Orientation string `json:"orientation"` // 横屏/竖屏/等比
Resolution string `json:"resolution"` // 分辨率档位
SizeText string `json:"sizeText"` // 人类可读体积
DurationText string `json:"durationText"` // 人类可读时长
}
// List POST /api/media/list 分页查询媒体库
func (c *MediaController) List(ctx *gin.Context) {
var req mediaListReq
if err := ctx.ShouldBindJSON(&req); err != nil {
Error(ctx, http.StatusBadRequest, "请求参数错误:"+err.Error())
return
}
if req.Page < 1 {
req.Page = 1
}
q := store.MediaQuery{
MediaType: req.MediaType,
Orientation: req.Orientation,
Keyword: req.Keyword,
Codec: req.Codec,
Format: req.Format,
Resolution: req.Resolution,
Page: req.Page,
PageSize: req.PageSize,
SortField: req.SortField,
SortOrder: req.SortOrder,
}
list, err := c.store.ListMedia(q)
if err != nil {
Error(ctx, http.StatusInternalServerError, "查询媒体列表失败:"+err.Error())
return
}
total, err := c.store.CountMedia(q)
if err != nil {
Error(ctx, http.StatusInternalServerError, "统计媒体数量失败:"+err.Error())
return
}
views := make([]interface{}, 0, len(list))
for _, m := range list {
views = append(views, toMediaView(m))
}
Success(ctx, map[string]interface{}{"list": views, "total": total})
}
// Stats POST /api/media/stats 媒体库统计看板
func (c *MediaController) Stats(ctx *gin.Context) {
st, err := c.store.Stats()
if err != nil {
Error(ctx, http.StatusInternalServerError, "统计失败:"+err.Error())
return
}
Success(ctx, st)
}
// toMediaView 补充派生字段
func toMediaView(m store.MediaInfo) mediaView {
v := mediaView{MediaInfo: m}
if m.Width > 0 && m.Height > 0 {
v.Orientation = classify.OrientationOf(m.Width, m.Height).String()
v.Resolution = classify.ResLabel(m.Width, m.Height)
}
if m.FileSize > 0 {
v.SizeText = humanize.Bytes(uint64(m.FileSize))
}
if m.Duration > 0 {
v.DurationText = formatDuration(m.Duration)
}
return v
}
// formatDuration 秒 → 时:分:秒
func formatDuration(sec float64) string {
s := int(sec)
h := s / 3600
m := (s % 3600) / 60
secs := s % 60
if h > 0 {
return fmt.Sprintf("%d:%02d:%02d", h, m, secs)
}
return fmt.Sprintf("%02d:%02d", m, secs)
}