refactor: 重构 GUI 框架为 Wails

- 添加 Go 后端实现,包括配置管理、文件上传逻辑和 Wails 应用接口
- 实现前端 Vue 界面,提供服务器配置、目录选择、上传控制等功能
- 集成 Element Plus 组件库构建用户界面
- 添加文件上传进度显示和实时日志输出功能
- 实现后台文件监控和上传任务管理
- 配置 Wails 框架支持前后端交互
- 更新项目依赖,移除 Fyne 框架,集成 Wails v2
- 添加项目配置文件管理和自动保存功能
This commit is contained in:
2026-04-27 00:34:24 +08:00
parent 4c13ecceaf
commit 26afd30e84
46 changed files with 4225 additions and 809 deletions
+310
View File
@@ -0,0 +1,310 @@
<script lang="ts" setup>
import {ref, nextTick, watch} from 'vue'
import {ElMessage} from 'element-plus'
import {ElMessageBox} from 'element-plus'
import {SelectPath, GetConfig, WriteConfig, StartUpload, StopUpload} from '../wailsjs/go/main/App';
import {config} from "../wailsjs/go/models.ts";
import Config = config.Config;
import {EventsOn, LogPrint} from "../wailsjs/runtime";
const serverUrl = ref('')
const token = ref('')
const checkDir = ref('')
const concurrentFiles = ref(1)
const uploadThreads = ref(1)
const progress = ref(0)
const isRunning = ref(false)
const logOutput = ref<string[]>([])
const logContentRef = ref<HTMLElement>()
interface FileProgress {
name: string
uploaded: number
total: number
percentage: number
}
const progressList = ref<FileProgress[]>([
// {name: '测试文件1.txt', uploaded: 100, total: 500, percentage: 20},
])
const addLog = (msg: string) => {
logOutput.value.push(`[${new Date().toLocaleString()}]` + msg)
nextTick(() => {
if (logContentRef.value) {
logContentRef.value.scrollTop = logContentRef.value.scrollHeight
}
})
}
const selectDirectory = () => {
// ElMessage.info('请手动输入检测目录路径')
SelectPath().then((path) => {
checkDir.value = path
})
}
const startRun = () => {
if (!serverUrl.value) {
ElMessage.warning('请输入服务器地址')
return
}
if (!token.value) {
ElMessage.warning('请输入Token')
return
}
if (!checkDir.value) {
ElMessage.warning('请选择检测目录')
return
}
isRunning.value = true
progress.value = 0
addLog("===============================================")
// addLog(`开始运行...`)
addLog(`服务器: ${serverUrl.value}`)
addLog(`检测目录: ${checkDir.value}`)
addLog(`同时处理文件数: ${concurrentFiles.value}`)
addLog(`单文件上传线程: ${uploadThreads.value}`)
addLog("===============================================")
StartUpload()
}
const stopRun = () => {
if (isRunning.value) {
isRunning.value = false
addLog(`正在停止运行`)
StopUpload().then(() => {
ElMessage.info('已停止运行')
})
}
}
const clearLog = () => {
ElMessageBox.confirm('确定要清除日志吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
logOutput.value = []
ElMessage.success('日志已清除')
}).catch(() => {
})
}
// 加载配置
try {
GetConfig().then((config: Config) => {
serverUrl.value = config.url
token.value = config.token
checkDir.value = config.check_dir
concurrentFiles.value = config.handle_file_count
uploadThreads.value = config.thread_count
LogPrint(`[${new Date().toLocaleString()}] 配置已加载`)
})
} catch (e) {
console.log(e)
}
watch(serverUrl, () => {
WriteConfig("url", serverUrl.value)
})
watch(token, () => {
WriteConfig("token", token.value)
})
watch(checkDir, () => {
WriteConfig("check-dir", checkDir.value)
})
watch(concurrentFiles, () => {
WriteConfig("handle-file-count", concurrentFiles.value)
})
watch(uploadThreads, () => {
WriteConfig("thread-count", uploadThreads.value)
})
EventsOn("log", (msg) => {
addLog(msg)
})
EventsOn("progress", (progress) => {
progressList.value = progress
})
</script>
<template>
<div class="container">
<div class="left-panel">
<div class="form-item">
<label>服务器地址</label>
<el-input v-model="serverUrl" placeholder="请输入服务器地址" :disabled="isRunning"/>
</div>
<div class="form-item">
<label>Token</label>
<el-input v-model="token" placeholder="请输入Token" :disabled="isRunning"/>
</div>
<div class="form-item">
<label>检测目录</label>
<div class="dir-input">
<el-input v-model="checkDir" placeholder="请选择检测目录" :disabled="isRunning"/>
<el-button @click="selectDirectory" :disabled="isRunning">选择目录</el-button>
</div>
</div>
<div class="form-item">
<label>同时处理文件数</label>
<el-input-number v-model="concurrentFiles" :min="1" :max="100" :disabled="isRunning"/>
</div>
<div class="form-item">
<label>单文件上传线程</label>
<el-input-number v-model="uploadThreads" :min="1" :max="100" :disabled="isRunning"/>
</div>
<div class="form-item">
<label>上传进度</label>
<div class="progress-list">
<div v-for="(item, index) in progressList" :key="index" class="progress-item">
<span class="file-name">{{ item.name }}</span>
<el-progress :percentage="item.percentage" :status="item.percentage === 100 ? 'success' : undefined"/>
<span class="progress-text">{{ item.uploaded }}/{{ item.total }}</span>
</div>
</div>
</div>
<div class="buttons">
<el-button type="primary" @click="startRun" :disabled="isRunning">开始运行</el-button>
<el-button type="danger" @click="stopRun" :disabled="!isRunning">停止运行</el-button>
<el-button @click="clearLog">清除日志</el-button>
</div>
</div>
<div class="right-panel">
<div class="log-header">日志输出</div>
<div class="log-content" ref="logContentRef">
<div v-for="(log, index) in logOutput" :key="index" class="log-line">{{ log }}</div>
</div>
</div>
</div>
</template>
<style scoped>
.container {
display: flex;
height: calc(100vh - 40px);
padding: 20px;
gap: 20px;
background-color: #f5f5f5;
}
.left-panel {
width: 350px;
min-width: 300px;
background: white;
padding: 20px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 16px;
}
.form-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-item label {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.dir-input {
display: flex;
gap: 8px;
}
.dir-input .el-input {
flex: 1;
}
.buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 10px;
}
.progress-list {
max-height: 160px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 10px;
}
.progress-item {
display: flex;
align-items: center;
}
.file-name {
width: 130px;
font-size: 12px;
color: #606266;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progress-item .el-progress {
margin-left: 8px;
flex: 1;
}
.progress-text {
width: 75px;
font-size: 12px;
color: #909399;
text-align: right;
}
.right-panel {
flex: 1;
background: white;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.log-header {
padding: 12px 20px;
border-bottom: 1px solid #e4e7ed;
font-size: 14px;
font-weight: 500;
color: #303133;
}
.log-content {
flex: 1;
padding: 5px 5px;
overflow-x: auto;
overflow-y: auto;
background: #1e1e1e;
font-family: Consolas, Monaco, 'Courier New', monospace;
font-size: 12px;
}
.log-line {
color: #dcdfe4;
line-height: 1.4;
white-space: nowrap;
}
.log-line:hover {
background: #2d2d2d;
}
</style>
+10
View File
@@ -0,0 +1,10 @@
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './style.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
+27
View File
@@ -0,0 +1,27 @@
html {
/*background-color: rgba(27, 38, 54, 1);*/
/*text-align: center;*/
/*color: white;*/
overflow: hidden;
}
body {
margin: 0;
/*color: white;*/
/*font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",*/
/*"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",*/
/*sans-serif;*/
}
/*@font-face {*/
/* font-family: "Nunito";*/
/* font-style: normal;*/
/* font-weight: 400;*/
/* !*src: local(""),*!*/
/* !*url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");*!*/
/*}*/
/*#app {*/
/* !*height: 100vh;*!*/
/* !*text-align: center;*!*/
/*}*/