From 14056ba7b9ab4afc115dc25fcef047d7623e39fd Mon Sep 17 00:00:00 2001 From: Odboy Date: Thu, 24 Apr 2025 21:05:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(scan):=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=89=AB=E6=8F=8F=E7=9B=AE=E5=BD=95=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在扫描页面添加目录输入框和选择按钮 - 实现文件系统浏览器,支持Windows盘符和目录浏览 - 添加扫描目录参数传递到后端服务 - 显示当前扫描目录路径信息 - 修复前端空数组访问错误 - 更新文档说明扫描功能变化 --- README.md | 4 +- frontend/src/App.vue | 230 +++++++++++++++--------------- frontend/src/views/Dashboard.vue | 4 +- frontend/src/views/Scan.vue | 178 ++++++++++++++++++++++- go.mod | 2 +- internal/api/fs_controller.go | 88 ++++++++++++ internal/api/fs_drives_other.go | 8 ++ internal/api/fs_drives_windows.go | 20 +++ internal/api/router.go | 3 + internal/api/scan_controller.go | 12 +- internal/service/scan.go | 42 ++++-- internal/store/store.go | 4 +- media-info.db | Bin 0 -> 36864 bytes 13 files changed, 460 insertions(+), 135 deletions(-) create mode 100644 internal/api/fs_controller.go create mode 100644 internal/api/fs_drives_other.go create mode 100644 internal/api/fs_drives_windows.go create mode 100644 media-info.db diff --git a/README.md b/README.md index 7e453fa..79ee550 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 功能特性 -- **图片 / 视频 / 音频**:扫描当前目录(递归),自动识别图片、视频与音频文件 +- **图片 / 视频 / 音频**:可指定任意目录进行扫描(递归),自动识别图片、视频与音频文件;不填目录时默认扫描程序当前运行目录 - **标准分辨率归类**:720P、1080P、2K、2.5K、影院 2K、DCI 4K、8K 及带鱼屏(UltraWide 2K/3K/4K)等标准规格单独建目录 - **非标分辨率归类**:按像素区间(每 1024px 一档)自动归入 1K ~ 19K / 普通 / 原图(原画质)目录 - **方向区分**:横屏、竖屏、等比(正方形)分开整理,GIF 单独处理 @@ -87,7 +87,7 @@ npm install && npm run dev | 页面 | 功能 | |------|------| -| 扫描整理 | 点击【开始扫描】触发扫描归类,实时显示运行日志与进度 | +| 扫描整理 | 点击「选择目录」浏览服务器磁盘(只列目录),也可手动输入;点击【开始扫描】触发扫描归类,实时显示运行日志与进度 | | 统计看板 | 媒体总数/总体积/重复文件、类型/方向/分辨率/编码/格式分布、体积 TOP10 | | 媒体库 | 按类型/方向/分辨率/编码/格式/文件名筛选,分页 + 列排序 | diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b783142..068bff9 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,215 +1,215 @@ diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 1662ce4..1107d76 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -59,12 +59,12 @@
分辨率分布
-
+
{{ item.key }} {{ item.count }}
-
暂无带尺寸的媒体
+
暂无带尺寸的媒体
diff --git a/frontend/src/views/Scan.vue b/frontend/src/views/Scan.vue index 7cfd2aa..d539747 100644 --- a/frontend/src/views/Scan.vue +++ b/frontend/src/views/Scan.vue @@ -6,10 +6,24 @@
- 点击开始后,程序将扫描当前运行目录并自动归类媒体文件(会移动文件,整理前请先备份)。 + 点击开始后,程序将扫描所选目录并自动归类媒体文件(会移动文件,整理前请先备份)。 整理期间请保持服务运行,结束后可在「媒体库」查看结果。
+
+
+ 扫描目录 + + 选择目录 + +
+
+
+ +
+
+ 盘符 + 上一级 + {{ fsData.current || '选择磁盘' }} +
+ +
+
+ + {{ d }} +
+
+ +
+
+ + {{ d.name }} +
+
此目录下没有子目录
+
+
+ +
+ 取消 + 选中此目录 +
+
+
运行日志 + 扫描目录:{{ status.rootDir }} 开始于 {{ status.started }}
@@ -62,14 +131,24 @@ export default { data() { return { starting: false, + scanDir: localStorage.getItem('scan-dir') || '', status: { state: 'idle', message: '', started: '', elapsed: 0, + rootDir: '', lines: [] }, - pollTimer: null + pollTimer: null, + dirPickerVisible: false, + dirLoading: false, + fsData: { + current: '', + parent: '', + drives: [], + dirs: [] + } } }, mounted() { @@ -99,7 +178,8 @@ export default { async handleStart() { this.starting = true try { - const res = await request.post('/api/scan/start') + localStorage.setItem('scan-dir', this.scanDir) + const res = await request.post('/api/scan/start', { dir: this.scanDir.trim() }) this.status = res.data || this.status this.startPoll() this.$message.success('已开始扫描整理') @@ -107,6 +187,27 @@ export default { this.starting = false } }, + openDirPicker() { + this.dirPickerVisible = true + this.loadFs(this.scanDir.trim()) + }, + async loadFs(dir) { + this.dirLoading = true + try { + const res = await request.get('/api/fs/list', { params: dir ? { dir } : {} }) + this.fsData = res.data || { current: '', parent: '', drives: [], dirs: [] } + } catch (e) { + // 拦截器已提示 + } finally { + this.dirLoading = false + } + }, + confirmDir() { + if (!this.fsData.current) return + this.scanDir = this.fsData.current + localStorage.setItem('scan-dir', this.scanDir) + this.dirPickerVisible = false + }, startPoll() { this.stopPoll() this.pollTimer = setInterval(() => this.loadStatus(), 1000) @@ -144,6 +245,77 @@ export default { margin-right: 4px; } +.scan-form { + margin-bottom: 20px; +} + +.form-row { + display: flex; + align-items: center; + gap: 12px; + max-width: 640px; +} + +.form-label { + flex-shrink: 0; + font-size: 13px; + font-weight: 600; + color: #1f3a6b; +} + +.dir-picker .dir-nav { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; +} + +.dir-picker .dir-path { + flex: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 13px; + color: #4a5a78; + background: #f5f7fa; + border-radius: 4px; + padding: 6px 10px; +} + +.dir-picker .dir-list { + height: 320px; + overflow-y: auto; + border: 1px solid #ebeef5; + border-radius: 6px; + padding: 6px; +} + +.dir-picker .dir-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + color: #303133; +} + +.dir-picker .dir-item:hover { + background: #f0f6ff; +} + +.dir-picker .dir-item .el-icon-folder { + color: #faad14; +} + +.dir-picker .dir-empty { + padding: 30px 0; + text-align: center; + color: #909399; + font-size: 13px; +} + .scan-actions { margin-bottom: 20px; } diff --git a/go.mod b/go.mod index c52fc23..2478070 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d github.com/satori/go.uuid v1.2.0 golang.org/x/image v0.16.0 + golang.org/x/sys v0.41.0 xorm.io/xorm v1.3.4 ) @@ -44,7 +45,6 @@ require ( golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect - golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.10 // indirect modernc.org/libc v1.22.2 // indirect diff --git a/internal/api/fs_controller.go b/internal/api/fs_controller.go new file mode 100644 index 0000000..ce8898a --- /dev/null +++ b/internal/api/fs_controller.go @@ -0,0 +1,88 @@ +// Package api 提供服务器文件系统浏览接口,供前端目录选择器使用(只列出目录,不列文件)。 +package api + +import ( + "net/http" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + + "github.com/gin-gonic/gin" +) + +// FsController 服务器文件系统浏览控制器 +type FsController struct{} + +// NewFsController 创建文件系统浏览控制器 +func NewFsController() *FsController { + return &FsController{} +} + +// FsListResult 目录浏览结果 +type FsListResult struct { + Current string `json:"current"` // 当前目录绝对路径;盘符层为空 + Parent string `json:"parent"` // 父目录;根/盘符层时等于 Current + Drives []string `json:"drives"` // Windows 盘符列表(非 Windows 为空) + Dirs []FsDir `json:"dirs"` // 子目录列表(不含文件) +} + +// FsDir 子目录项 +type FsDir struct { + Name string `json:"name"` + Path string `json:"path"` +} + +// List GET /api/fs/list?dir= 列出指定目录的子目录(不含文件); +// dir 为空时返回 Windows 盘符列表或 Unix 根目录。 +func (c *FsController) List(ctx *gin.Context) { + dir := strings.TrimSpace(ctx.Query("dir")) + + if dir == "" { + if runtime.GOOS == "windows" { + Success(ctx, FsListResult{Drives: listLogicalDrives()}) + return + } + dir = string(os.PathSeparator) + } + + abs, err := filepath.Abs(dir) + if err != nil { + Error(ctx, http.StatusBadRequest, "路径解析失败: "+err.Error()) + return + } + st, err := os.Stat(abs) + if err != nil || !st.IsDir() { + Error(ctx, http.StatusBadRequest, "目录不存在或不可访问: "+abs) + return + } + + entries, err := os.ReadDir(abs) + if err != nil { + Error(ctx, http.StatusBadRequest, "读取目录失败: "+err.Error()) + return + } + + parent := filepath.Dir(abs) + if parent == abs { + parent = abs + } + dirs := make([]FsDir, 0, len(entries)) + for _, e := range entries { + if !e.IsDir() { + continue + } + dirs = append(dirs, FsDir{Name: e.Name(), Path: filepath.Join(abs, e.Name())}) + } + sort.Slice(dirs, func(i, j int) bool { + return strings.ToLower(dirs[i].Name) < strings.ToLower(dirs[j].Name) + }) + + Success(ctx, FsListResult{ + Current: abs, + Parent: parent, + Drives: nil, + Dirs: dirs, + }) +} diff --git a/internal/api/fs_drives_other.go b/internal/api/fs_drives_other.go new file mode 100644 index 0000000..267b1ca --- /dev/null +++ b/internal/api/fs_drives_other.go @@ -0,0 +1,8 @@ +//go:build !windows + +package api + +// listLogicalDrives 非 Windows 平台无盘符概念,返回空 +func listLogicalDrives() []string { + return nil +} diff --git a/internal/api/fs_drives_windows.go b/internal/api/fs_drives_windows.go new file mode 100644 index 0000000..c3e4a03 --- /dev/null +++ b/internal/api/fs_drives_windows.go @@ -0,0 +1,20 @@ +//go:build windows + +package api + +import "golang.org/x/sys/windows" + +// listLogicalDrives 返回 Windows 所有逻辑盘符(如 C:\、D:\) +func listLogicalDrives() []string { + bits, err := windows.GetLogicalDrives() + if err != nil { + return nil + } + drives := make([]string, 0, 26) + for i := 0; i < 26; i++ { + if bits&(1<68y=)gY9j*&BAeP`uC{X}Yex7DCfB zL+u-CkGJT2B|cHV`aj1H9U9v6hF(xt*EfG_g+GNKg=gD8w-2{|ZJntCHUtnr009IL zKmY**{*%DXx>0;p(!V|ly1u-v99=ja=?9(@bep~DVR+YdFwuCQIiYi>C#$p zZ%@Ary^n1<8no5I%o)kSwd^+LPnV}bcKJ=!5_5HTyIx1`t2@naPj#bMD(T-h#%(4Y zO^%jQEzM2rz<1RPVO#I4x;U*@j?48+@!q-=<#YR_Qd4&xTQytMPSk#0t?mnVHYtT| zU8GCNNbAb