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
+205
View File
@@ -0,0 +1,205 @@
<template>
<div class="page-card">
<div class="toolbar">
<span class="title">扫描整理</span>
</div>
<div class="scan-tip">
<i class="el-icon-warning-outline"></i>
点击开始后,程序将扫描当前运行目录并自动归类媒体文件(<b>会移动文件</b>,整理前请先备份)。
整理期间请保持服务运行,结束后可在「媒体库」查看结果。
</div>
<div class="scan-actions">
<el-button
v-if="status.state !== 'running'"
type="primary"
size="medium"
icon="el-icon-video-play"
:loading="starting"
@click="handleStart">开始扫描</el-button>
<div v-else class="running-hint">
<i class="el-icon-loading"></i>
<span>扫描整理中,已运行 {{ status.elapsed }} 秒...</span>
</div>
</div>
<el-alert
v-if="status.state === 'done'"
title="扫描整理完成"
type="success"
:description="status.message"
show-icon
:closable="false"
class="state-alert"/>
<el-alert
v-if="status.state === 'error'"
title="扫描整理失败"
type="error"
:description="status.message"
show-icon
:closable="false"
class="state-alert"/>
<div class="log-panel">
<div class="log-header">
<span>运行日志</span>
<span class="muted" v-if="status.started">开始于 {{ status.started }}</span>
</div>
<div ref="logBox" class="log-body" v-loading="status.state === 'running'">
<div v-if="status.lines.length === 0" class="muted empty-log">暂无日志,点击「开始扫描」后在此查看</div>
<div v-for="(line, idx) in status.lines" :key="idx" class="log-line">{{ line }}</div>
</div>
</div>
</div>
</template>
<script>
import request from '../utils/request'
export default {
name: 'Scan',
data() {
return {
starting: false,
status: {
state: 'idle',
message: '',
started: '',
elapsed: 0,
lines: []
},
pollTimer: null
}
},
mounted() {
this.loadStatus()
},
destroyed() {
this.stopPoll()
},
methods: {
async loadStatus() {
try {
const res = await request.get('/api/scan/status')
const data = res.data || {}
const wasRunning = this.status.state === 'running'
const isRunning = data.state === 'running'
this.status = data
if (isRunning && !wasRunning) this.startPoll()
if (wasRunning && !isRunning) {
this.stopPoll()
this.$message.success(data.message || '扫描完成')
}
if (isRunning) this.scrollBottom()
} catch (e) {
// 拦截器已提示
}
},
async handleStart() {
this.starting = true
try {
const res = await request.post('/api/scan/start')
this.status = res.data || this.status
this.startPoll()
this.$message.success('已开始扫描整理')
} finally {
this.starting = false
}
},
startPoll() {
this.stopPoll()
this.pollTimer = setInterval(() => this.loadStatus(), 1000)
},
stopPoll() {
if (this.pollTimer) {
clearInterval(this.pollTimer)
this.pollTimer = null
}
},
scrollBottom() {
this.$nextTick(() => {
const box = this.$refs.logBox
if (box) box.scrollTop = box.scrollHeight
})
}
}
}
</script>
<style scoped>
.scan-tip {
background: #f8fbff;
border: 1px solid #e6efff;
border-radius: 8px;
padding: 12px 16px;
color: #4a5a78;
font-size: 13px;
line-height: 1.8;
margin-bottom: 20px;
}
.scan-tip .el-icon-warning-outline {
color: #faad14;
margin-right: 4px;
}
.scan-actions {
margin-bottom: 20px;
}
.running-hint {
display: inline-flex;
align-items: center;
gap: 8px;
color: #1677ff;
font-size: 14px;
}
.state-alert {
margin-bottom: 16px;
}
.log-panel {
border: 1px solid #e6efff;
border-radius: 8px;
overflow: hidden;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background: #f0f6ff;
font-size: 13px;
font-weight: 600;
color: #1f3a6b;
}
.log-body {
height: 380px;
overflow-y: auto;
padding: 12px 16px;
background: #0d1b2e;
font-family: Menlo, Consolas, monospace;
font-size: 12px;
line-height: 1.7;
}
.log-line {
color: #b7d0f2;
white-space: pre-wrap;
word-break: break-all;
}
.empty-log {
color: #6b8bb8;
}
.muted {
color: #c0c4cc;
font-weight: 400;
font-size: 12px;
}
</style>