feat(scan): 添加自定义扫描目录选择功能

- 在扫描页面添加目录输入框和选择按钮
- 实现文件系统浏览器,支持Windows盘符和目录浏览
- 添加扫描目录参数传递到后端服务
- 显示当前扫描目录路径信息
- 修复前端空数组访问错误
- 更新文档说明扫描功能变化
This commit is contained in:
2025-04-24 21:05:00 +08:00
parent e053b378fb
commit 14056ba7b9
13 changed files with 460 additions and 135 deletions
+175 -3
View File
@@ -6,10 +6,24 @@
<div class="scan-tip">
<i class="el-icon-warning-outline"></i>
点击开始后,程序将扫描当前运行目录并自动归类媒体文件(<b>会移动文件</b>,整理前请先备份)。
点击开始后,程序将扫描所选目录并自动归类媒体文件(<b>会移动文件</b>,整理前请先备份)。
整理期间请保持服务运行,结束后可在「媒体库」查看结果。
</div>
<div class="scan-form">
<div class="form-row">
<span class="form-label">扫描目录</span>
<el-input
v-model="scanDir"
placeholder="留空则扫描程序当前运行目录,例如 D:\media 或 /Users/me/Pictures"
clearable
size="medium"
@keyup.enter.native="handleStart">
<el-button slot="append" icon="el-icon-folder-opened" @click="openDirPicker">选择目录</el-button>
</el-input>
</div>
</div>
<div class="scan-actions">
<el-button
v-if="status.state !== 'running'"
@@ -41,9 +55,64 @@
:closable="false"
class="state-alert"/>
<el-dialog
title="选择要整理的目录"
:visible.sync="dirPickerVisible"
width="520px"
append-to-body>
<div class="dir-picker">
<div class="dir-nav">
<el-button
v-if="fsData.drives && fsData.drives.length"
size="mini"
icon="el-icon-refresh"
@click="loadFs('')">盘符</el-button>
<el-button
v-else-if="fsData.current"
size="mini"
icon="el-icon-back"
:disabled="fsData.parent === fsData.current"
@click="loadFs(fsData.parent)">上一级</el-button>
<span class="dir-path" :title="fsData.current">{{ fsData.current || '选择磁盘' }}</span>
</div>
<div v-if="fsData.drives && fsData.drives.length" class="dir-list">
<div
v-for="d in fsData.drives"
:key="d"
class="dir-item"
@click="loadFs(d)">
<i class="el-icon-folder"></i>
<span>{{ d }}</span>
</div>
</div>
<div v-else class="dir-list" v-loading="dirLoading">
<div
v-for="d in fsData.dirs"
:key="d.path"
class="dir-item"
@dblclick="loadFs(d.path)">
<i class="el-icon-folder"></i>
<span>{{ d.name }}</span>
</div>
<div v-if="!dirLoading && fsData.dirs.length === 0" class="dir-empty">此目录下没有子目录</div>
</div>
</div>
<div slot="footer">
<el-button @click="dirPickerVisible = false">取消</el-button>
<el-button
type="primary"
:disabled="!fsData.current"
@click="confirmDir">选中此目录</el-button>
</div>
</el-dialog>
<div class="log-panel">
<div class="log-header">
<span>运行日志</span>
<span class="muted" v-if="status.rootDir">扫描目录:{{ status.rootDir }}</span>
<span class="muted" v-if="status.started">开始于 {{ status.started }}</span>
</div>
<div ref="logBox" class="log-body" v-loading="status.state === 'running'">
@@ -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;
}