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:
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Package api 提供 Web 管理接口(Gin),遵循统一响应与分页约定。
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Response 统一响应结构
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// Success 成功响应
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: http.StatusOK,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Error 失败响应(仅允许 400 / 500)
|
||||
func Error(c *gin.Context, statusCode int, message string) {
|
||||
c.JSON(statusCode, Response{
|
||||
Code: statusCode,
|
||||
Message: message,
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// PageResult 分页响应体
|
||||
type PageResult struct {
|
||||
List []interface{} `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"kenaito-media-picker/internal/service"
|
||||
"kenaito-media-picker/internal/store"
|
||||
)
|
||||
|
||||
// NewRouter 组装 Gin 路由:健康检查 + API 路由组
|
||||
func NewRouter(st *store.Store) *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
// 健康检查
|
||||
r.GET("/health/check", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
apiGroup := r.Group("/api")
|
||||
{
|
||||
mc := NewMediaController(st)
|
||||
apiGroup.POST("/media/list", mc.List)
|
||||
apiGroup.POST("/media/stats", mc.Stats)
|
||||
|
||||
sc := NewScanController(service.NewScanService(st))
|
||||
apiGroup.POST("/scan/start", sc.Start)
|
||||
apiGroup.GET("/scan/status", sc.Status)
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"kenaito-media-picker/internal/service"
|
||||
)
|
||||
|
||||
// ScanController 扫描整理接口
|
||||
type ScanController struct {
|
||||
service *service.ScanService
|
||||
}
|
||||
|
||||
// NewScanController 创建扫描控制器
|
||||
func NewScanController(svc *service.ScanService) *ScanController {
|
||||
return &ScanController{service: svc}
|
||||
}
|
||||
|
||||
// Start POST /api/scan/start 触发一次扫描整理
|
||||
func (c *ScanController) Start(ctx *gin.Context) {
|
||||
if err := c.service.Start(); err != nil {
|
||||
Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
Success(ctx, c.service.Status())
|
||||
}
|
||||
|
||||
// Status GET /api/scan/status 查询扫描状态与日志
|
||||
func (c *ScanController) Status(ctx *gin.Context) {
|
||||
Success(ctx, c.service.Status())
|
||||
}
|
||||
Reference in New Issue
Block a user