@@ -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<