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,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>
|
||||
Reference in New Issue
Block a user