mirror of
https://gitee.com/czh-dev/upload-hub
synced 2026-07-16 02:11:19 +08:00
feat:新增文件工具箱
- 本地查找 - 文件树状图 - Diff - 文件加密 - 批量修改文件名 - 文件格式转换
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
package cn.czh.controller;
|
||||
|
||||
import cn.czh.base.Result;
|
||||
import cn.czh.service.IFileSearchService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RequestMapping("/fileSearch")
|
||||
@RestController
|
||||
public class FileSearchController {
|
||||
|
||||
@Resource
|
||||
private IFileSearchService fileSearchService;
|
||||
|
||||
/**
|
||||
* 获取系统磁盘列表
|
||||
*/
|
||||
@GetMapping("/getDrives")
|
||||
public Result<?> getDrives() {
|
||||
return Result.success(fileSearchService.getDrives());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定的磁盘列表构建文件索引
|
||||
*/
|
||||
@PostMapping("/buildIndex")
|
||||
public Result<?> buildIndex(@RequestBody List<String> drives) {
|
||||
return Result.success(fileSearchService.buildIndex(drives));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件名称模糊查询
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
public Result<?> search(@RequestParam String keyword) {
|
||||
return Result.success(fileSearchService.searchFiles(keyword));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package cn.czh.controller;
|
||||
|
||||
import cn.czh.base.Result;
|
||||
import cn.czh.service.ILocalFileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RequestMapping("/localFile")
|
||||
@RestController
|
||||
public class LocalSearchController {
|
||||
|
||||
@Resource
|
||||
private ILocalFileService fileSearchService;
|
||||
|
||||
/**
|
||||
* 获取系统磁盘列表
|
||||
*/
|
||||
@GetMapping("/getDrives")
|
||||
public Result<?> getDrives() {
|
||||
return Result.success(fileSearchService.getDrives());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定的磁盘列表构建文件索引
|
||||
*/
|
||||
@PostMapping("/buildIndex")
|
||||
public Result<?> buildIndex(@RequestBody List<String> drives) {
|
||||
return Result.success(fileSearchService.buildIndex(drives));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件名称模糊查询
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
public Result<?> search(@RequestParam String keyword) {
|
||||
return Result.success(fileSearchService.searchFiles(keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件地址打开目录
|
||||
*/
|
||||
@PostMapping("/openDir")
|
||||
public Result<?> openDir(@RequestParam String dir) {
|
||||
try {
|
||||
Runtime.getRuntime().exec("explorer.exe /select," + dir);
|
||||
return Result.success();
|
||||
} catch (Exception e) {
|
||||
log.error("打开目录失败:{}", e.getMessage(), e);
|
||||
return Result.error("打开目录失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件目录展示文件树
|
||||
*/
|
||||
@GetMapping("/getFileTree")
|
||||
public Result<?> getFileTree(
|
||||
@RequestParam String path,
|
||||
@RequestParam(defaultValue = "true") boolean showFiles,
|
||||
@RequestParam(defaultValue = "true") boolean showFolders,
|
||||
@RequestParam(defaultValue = "3") int maxDepth
|
||||
|
||||
) {
|
||||
// 避免直接输入磁盘进去全盘扫描
|
||||
if (path.length() == 2 && path.endsWith(":")) {
|
||||
return Result.error("请选择目录");
|
||||
}
|
||||
|
||||
return Result.success(fileSearchService.getFileTree(path, showFiles, showFolders, maxDepth));
|
||||
}
|
||||
}
|
||||
12
upload-file-backend/src/main/java/cn/czh/dto/FileNode.java
Normal file
12
upload-file-backend/src/main/java/cn/czh/dto/FileNode.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package cn.czh.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class FileNode {
|
||||
private String name; // 文件/文件夹名称
|
||||
private String path; // 绝对路径
|
||||
private boolean isFolder; // 是否是文件夹
|
||||
private List<FileNode> children; // 子节点
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package cn.czh.service;
|
||||
|
||||
import cn.czh.dto.FileNode;
|
||||
import cn.czh.dto.IndexResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IFileSearchService {
|
||||
public interface ILocalFileService {
|
||||
|
||||
List<String> getDrives();
|
||||
|
||||
@@ -12,4 +13,6 @@ public interface IFileSearchService {
|
||||
|
||||
List<String> searchFiles(String keyword);
|
||||
|
||||
FileNode getFileTree(String path, boolean showFiles, boolean showFolders, int maxDepth);
|
||||
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package cn.czh.service.impl;
|
||||
|
||||
import cn.czh.dto.FileNode;
|
||||
import cn.czh.dto.IndexResult;
|
||||
import cn.czh.service.IFileSearchService;
|
||||
import cn.czh.service.ILocalFileService;
|
||||
import cn.czh.utils.FileSearchUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -14,7 +15,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class FileSearchServiceImpl implements IFileSearchService {
|
||||
public class LocalFileServiceImpl implements ILocalFileService {
|
||||
|
||||
@Resource
|
||||
private FileSearchUtil fileSearchUtil;
|
||||
@@ -76,4 +77,44 @@ public class FileSearchServiceImpl implements IFileSearchService {
|
||||
public List<String> searchFiles(String keyword) {
|
||||
return fileSearchUtil.search(keyword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileNode getFileTree(String path, boolean showFiles, boolean showFolders, int maxDepth) {
|
||||
File root = new File(path);
|
||||
if (!root.exists() || !root.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
return buildFileTree(root, showFiles, showFolders, 0, maxDepth);
|
||||
}
|
||||
|
||||
private FileNode buildFileTree(File file, boolean showFiles, boolean showFolders, int currentDepth, int maxDepth) {
|
||||
// 判断是否匹配类型
|
||||
boolean matchesType = (file.isDirectory() && showFolders) || (!file.isDirectory() && showFiles);
|
||||
|
||||
// 如果不匹配类型,则不包含此节点
|
||||
if (!matchesType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
FileNode node = new FileNode();
|
||||
node.setName(file.getName());
|
||||
node.setPath(file.getAbsolutePath());
|
||||
node.setFolder(file.isDirectory());
|
||||
|
||||
// 如果是目录且未达到最大深度,递归构建子节点
|
||||
if (file.isDirectory() && (maxDepth == 0 || currentDepth < maxDepth)) {
|
||||
File[] files = file.listFiles();
|
||||
if (files != null) {
|
||||
List<FileNode> children = new ArrayList<>();
|
||||
for (File f : files) {
|
||||
FileNode child = buildFileTree(f, showFiles, showFolders, currentDepth + 1, maxDepth);
|
||||
if (child != null) {
|
||||
children.add(child);
|
||||
}
|
||||
}
|
||||
node.setChildren(children.isEmpty() ? null : children);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,15 @@
|
||||
"axios": "^1.8.1",
|
||||
"axios-extra": "^0.0.8",
|
||||
"core-js": "^3.8.3",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"diff2html": "^3.4.51",
|
||||
"element-ui": "^2.15.14",
|
||||
"os-browserify": "^0.3.0",
|
||||
"pdfjs-dist": "^5.0.375",
|
||||
"promise-queue-plus": "^1.2.2",
|
||||
"spark-md5": "^3.0.2",
|
||||
"vue": "^2.6.14",
|
||||
"vue-diff": "^1.2.4",
|
||||
"vue-router": "^3.5.1",
|
||||
"vue-virtual-scroller": "^1.1.2",
|
||||
"vuex": "^3.6.2"
|
||||
|
||||
164
upload-file-frontend/src/components/Toolbox/FileToolbox.vue
Normal file
164
upload-file-frontend/src/components/Toolbox/FileToolbox.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<!-- Toolbox.vue -->
|
||||
<template>
|
||||
<div class="toolbox-container">
|
||||
<el-row :gutter="20" class="tools-grid">
|
||||
<el-col :span="6" v-for="(tool, index) in tools" :key="index">
|
||||
<el-card
|
||||
class="tool-card"
|
||||
@click.native="openTool(tool)"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="tool-item">
|
||||
<i :class="tool.icon" class="tool-icon"></i>
|
||||
<span class="tool-name">{{ tool.name }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 动态模态框 -->
|
||||
<el-dialog
|
||||
:title="currentTool ? currentTool.name : ''"
|
||||
:visible.sync="dialogVisible"
|
||||
width="60%"
|
||||
:before-close="handleClose"
|
||||
custom-class="tool-dialog"
|
||||
>
|
||||
<component v-if="currentTool" :is="currentTool.component" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FileTree from './tools/FileTree.vue'
|
||||
import FileDiff from './tools/FileDiff.vue'
|
||||
import FileEncrypt from './tools/FileEncrypt.vue'
|
||||
import BatchRename from './tools/BatchRename.vue'
|
||||
import FormatConvert from './tools/FormatConvert.vue'
|
||||
import LocalSearch from './tools/LocalSearch.vue'
|
||||
|
||||
export default {
|
||||
name: 'FileToolbox',
|
||||
components: {
|
||||
FileTree,
|
||||
FileDiff,
|
||||
FileEncrypt,
|
||||
BatchRename,
|
||||
FormatConvert,
|
||||
LocalSearch,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tools: [
|
||||
{ name: '本地查找', icon: 'el-icon-search', component: 'LocalSearch', color: '#7232A8' },
|
||||
{ name: '文件树状图', icon: 'el-icon-folder-opened', component: 'FileTree', color: '#409EFF' },
|
||||
{ name: 'Diff比较', icon: 'el-icon-document-copy', component: 'FileDiff', color: '#67C23A' },
|
||||
{ name: '文件加密', icon: 'el-icon-lock', component: 'FileEncrypt', color: '#E6A23C' },
|
||||
{ name: '批量修改文件名', icon: 'el-icon-edit', component: 'BatchRename', color: '#F56C6C' },
|
||||
{ name: '文件格式转换', icon: 'el-icon-refresh', component: 'FormatConvert', color: '#909399' }
|
||||
],
|
||||
currentTool: null,
|
||||
dialogVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openTool(tool) {
|
||||
this.currentTool = tool
|
||||
this.dialogVisible = true
|
||||
},
|
||||
handleClose(done) {
|
||||
this.$confirm('确定关闭工具吗?')
|
||||
.then(() => {
|
||||
this.dialogVisible = false
|
||||
this.currentTool = null
|
||||
done()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbox-container {
|
||||
padding: 30px;
|
||||
height: 400px;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.tools-grid {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 25px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 15px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tool-card:hover .tool-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.tool-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: v-bind('tools[$index].color');
|
||||
}
|
||||
|
||||
/* 自定义模态框样式 */
|
||||
:deep(.tool-dialog) {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.tool-dialog .el-dialog__header) {
|
||||
background: #f5f7fa;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
:deep(.tool-dialog .el-dialog__title) {
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.tool-dialog .el-dialog__body) {
|
||||
padding: 20px;
|
||||
background: #ffffff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="batch-rename">
|
||||
<el-upload
|
||||
drag
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<el-input v-model="renameRule" placeholder="输入重命名规则" class="rename-input"></el-input>
|
||||
<el-button type="primary" @click="rename" class="action-btn">执行重命名</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BatchRename',
|
||||
data() {
|
||||
return {
|
||||
renameRule: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFileChange(file, fileList) {
|
||||
console.log('文件列表:', fileList)
|
||||
},
|
||||
rename() {
|
||||
this.$message.success('批量重命名功能待实现')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.batch-rename {
|
||||
padding: 10px;
|
||||
}
|
||||
.rename-input {
|
||||
margin: 15px 0;
|
||||
}
|
||||
.action-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="file-diff">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-input type="textarea" v-model="text1" placeholder="输入第一个文件内容" :rows="8"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-input type="textarea" v-model="text2" placeholder="输入第二个文件内容" :rows="8"></el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-button type="primary" @click="compare" class="action-btn">比较</el-button>
|
||||
|
||||
<div v-if="diffHtml" v-html="diffHtml" class="diff-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createTwoFilesPatch } from 'diff';
|
||||
import { Diff2Html } from 'diff2html';
|
||||
import 'diff2html/bundles/css/diff2html.min.css';
|
||||
|
||||
export default {
|
||||
name: 'FileDiff',
|
||||
data() {
|
||||
return {
|
||||
text1: '',
|
||||
text2: '',
|
||||
diffHtml: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
compare() {
|
||||
// 生成 Diff
|
||||
const diff = createTwoFilesPatch('File1', 'File2', this.text1, this.text2);
|
||||
|
||||
// 解析 Diff 并生成 HTML
|
||||
this.diffHtml = Diff2Html.getPrettyHtml(diff, {
|
||||
inputFormat: 'diff',
|
||||
outputFormat: 'side-by-side', // 或 "line-by-line"
|
||||
drawFileList: false
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-diff {
|
||||
padding: 10px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.diff-container {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="file-encrypt">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="文件内容">
|
||||
<el-input v-model="form.content" type="textarea" :rows="5"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="form.password" type="password"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="encrypt" class="action-btn">加密</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'FileEncrypt',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
content: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
encrypt() {
|
||||
this.$message.success('加密功能待实现')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-encrypt {
|
||||
padding: 10px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-left: 100px;
|
||||
}
|
||||
</style>
|
||||
275
upload-file-frontend/src/components/Toolbox/tools/FileTree.vue
Normal file
275
upload-file-frontend/src/components/Toolbox/tools/FileTree.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<div class="file-tree-container">
|
||||
<!-- 目录路径输入框和获取按钮 -->
|
||||
<div class="input-section">
|
||||
<el-input
|
||||
v-model="directoryPath"
|
||||
placeholder="请输入或选择目录路径"
|
||||
style="width: 300px; margin-right: 10px;"
|
||||
></el-input>
|
||||
<el-input
|
||||
v-model.number="maxDepth"
|
||||
type="number"
|
||||
placeholder="输入目录深度"
|
||||
style="width: 120px; margin-right: 10px;"
|
||||
:min="0"
|
||||
></el-input>
|
||||
<el-button type="primary" @click="fetchFileTree">获取文件树</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 过滤选项 -->
|
||||
<div class="filter-section" style="margin-top: 20px;">
|
||||
<el-input
|
||||
v-model="filterText"
|
||||
placeholder="输入关键字过滤"
|
||||
style="width: 200px; margin-right: 20px;"
|
||||
></el-input>
|
||||
<el-checkbox v-model="showFiles">显示文件</el-checkbox>
|
||||
<el-checkbox v-model="showFolders">显示文件夹</el-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- 文件树和文本展示区域 -->
|
||||
<div class="content-section" style="margin-top: 20px;">
|
||||
<!-- 文件树 -->
|
||||
<div class="tree-section">
|
||||
<el-tree
|
||||
:data="fileTreeData"
|
||||
:props="defaultProps"
|
||||
:filter-node-method="filterNode"
|
||||
ref="tree"
|
||||
node-key="path"
|
||||
default-expand-all
|
||||
class="tree-container"
|
||||
>
|
||||
<span slot-scope="{ node, data }" class="custom-tree-node">
|
||||
<i :class="data.folder ? 'el-icon-folder' : 'el-icon-document'"></i>
|
||||
<span style="margin-left: 8px;">{{ node.label }}</span>
|
||||
</span>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<!-- 文本树展示 -->
|
||||
<div class="text-section">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="textTree"
|
||||
placeholder="文件树文本格式将显示在这里"
|
||||
class="text-tree-input"
|
||||
readonly
|
||||
></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getFileTree } from '@/utils/api';
|
||||
|
||||
export default {
|
||||
name: 'FileTreeComponent',
|
||||
data() {
|
||||
return {
|
||||
directoryPath: '',
|
||||
filterText: '',
|
||||
showFiles: true,
|
||||
showFolders: true,
|
||||
maxDepth: 3, // 默认深度为 0,表示无限制
|
||||
fileTreeData: [],
|
||||
textTree: '',
|
||||
defaultProps: {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
filterText(val) {
|
||||
this.$refs.tree.filter(val);
|
||||
this.$nextTick(() => {
|
||||
this.updateTextTree();
|
||||
});
|
||||
},
|
||||
showFiles() {
|
||||
if (this.directoryPath) this.fetchFileTree();
|
||||
},
|
||||
showFolders() {
|
||||
if (this.directoryPath) this.fetchFileTree();
|
||||
},
|
||||
maxDepth() {
|
||||
if (this.directoryPath) this.fetchFileTree(); // 深度变化时重新获取
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async fetchFileTree() {
|
||||
if (!this.directoryPath) {
|
||||
this.$message.warning('请输入目录路径');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await getFileTree(this.directoryPath, this.showFiles, this.showFolders, this.maxDepth);
|
||||
if (res.code === 200) {
|
||||
this.fileTreeData = [res.data];
|
||||
this.$nextTick(() => {
|
||||
this.updateTextTree();
|
||||
});
|
||||
this.$message.success('文件树获取成功');
|
||||
} else {
|
||||
this.$message.error(res.msg || '获取文件树失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('请求失败,请检查网络或路径');
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
filterNode(value, data) {
|
||||
if (!value) return true;
|
||||
return data.name.toLowerCase().includes(value.toLowerCase());
|
||||
},
|
||||
updateTextTree() {
|
||||
const visibleNodes = this.getVisibleNodes();
|
||||
this.generateTextTreeFromVisibleNodes(visibleNodes);
|
||||
},
|
||||
getVisibleNodes() {
|
||||
const tree = this.$refs.tree;
|
||||
const allNodes = [];
|
||||
|
||||
const collectVisibleNodes = (node) => {
|
||||
if (!node) return;
|
||||
if (node.visible) {
|
||||
const nodeData = { ...node.data };
|
||||
if (node.childNodes && node.childNodes.length > 0) {
|
||||
nodeData.children = [];
|
||||
node.childNodes.forEach(child => {
|
||||
if (child.visible) {
|
||||
nodeData.children.push(collectVisibleNodes(child));
|
||||
}
|
||||
});
|
||||
if (nodeData.children.length === 0) delete nodeData.children;
|
||||
}
|
||||
return nodeData;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
tree.root.childNodes.forEach(rootNode => {
|
||||
const visibleNode = collectVisibleNodes(rootNode);
|
||||
if (visibleNode) allNodes.push(visibleNode);
|
||||
});
|
||||
|
||||
return allNodes.length > 0 ? { name: '', children: allNodes } : { name: '' };
|
||||
},
|
||||
generateTextTreeFromVisibleNodes(node, level = 0, isLast = false, prefix = '') {
|
||||
let result = '';
|
||||
if (!node) return result;
|
||||
|
||||
if (level === 0 && !node.name) {
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach((child, index) => {
|
||||
const isChildLast = index === node.children.length - 1;
|
||||
result += this.generateTextTreeFromVisibleNodes(child, 0, isChildLast, '');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const indent = ' '.repeat(level * 2);
|
||||
const connector = isLast ? '└── ' : '├── ';
|
||||
result += `${prefix}${indent}${connector}${node.name}\n`;
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
const newPrefix = prefix + indent + (isLast ? ' ' : '│ ');
|
||||
node.children.forEach((child, index) => {
|
||||
const isChildLast = index === node.children.length - 1;
|
||||
result += this.generateTextTreeFromVisibleNodes(child, level + 1, isChildLast, newPrefix);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.textTree = result.trim();
|
||||
return result;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-tree-container {
|
||||
padding: 20px;
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-section,
|
||||
.filter-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tree-section {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.text-section {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.text-tree-input {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.text-tree-input >>> .el-textarea__inner {
|
||||
height: 100%;
|
||||
resize: none;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
color: #303133;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.text-tree-input >>> .el-textarea__inner:hover {
|
||||
border-color: #c0c4cc;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.text-tree-input >>> .el-textarea__inner::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.text-tree-input >>> .el-textarea__inner::-webkit-scrollbar-thumb {
|
||||
background-color: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.text-tree-input >>> .el-textarea__inner::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #a8a8a8;
|
||||
}
|
||||
|
||||
.text-tree-input >>> .el-textarea__inner::-webkit-scrollbar-track {
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="format-convert">
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<el-select v-model="targetFormat" placeholder="选择目标格式" class="format-select">
|
||||
<el-option
|
||||
v-for="format in formatOptions"
|
||||
:key="format.value"
|
||||
:label="format.label"
|
||||
:value="format.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="convert" class="action-btn">转换</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'FormatConvert',
|
||||
data() {
|
||||
return {
|
||||
targetFormat: '',
|
||||
formatOptions: [
|
||||
{ value: 'pdf', label: 'PDF' },
|
||||
{ value: 'docx', label: 'Word' },
|
||||
{ value: 'txt', label: 'TXT' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFileChange(file) {
|
||||
console.log('待转换文件:', file)
|
||||
},
|
||||
convert() {
|
||||
this.$message.success('格式转换功能待实现')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.format-convert {
|
||||
padding: 10px;
|
||||
}
|
||||
.format-select {
|
||||
margin: 15px 0;
|
||||
width: 200px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="local-search">
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="选择磁盘:">
|
||||
<el-checkbox-group v-model="searchForm.drives">
|
||||
<el-checkbox v-for="drive in availableDrives" :key="drive" :label="drive" />
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键字:" style="margin-left: 30px;">
|
||||
<el-input v-model="searchForm.keyword" placeholder="输入关键字" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="searchFiles" :loading="searching">查找</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 搜索结果区域 -->
|
||||
<div class="results-area">
|
||||
<!-- 圆形进度条 -->
|
||||
<div v-if="searching" class="progress-container">
|
||||
<el-progress type="circle" :percentage="progress" :width="120" :stroke-width="8" color="#409EFF" />
|
||||
</div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<el-table v-else-if="searchResults.length > 0" :data="searchResults" style="width: 100%" max-height="400" stripe>
|
||||
<el-table-column prop="name" label="文件名" width="200" />
|
||||
<el-table-column prop="path" label="路径" />
|
||||
<el-table-column label="操作" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" @click="openLocalDir(scope.row.path)">位置</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 无结果提示 -->
|
||||
<div v-else-if="searched" class="no-results">
|
||||
未找到匹配的文件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getLocalDrives, buildIndex, localFileSearch, openDir } from '@/utils/api'
|
||||
export default {
|
||||
name: 'LocalSearch',
|
||||
data() {
|
||||
return {
|
||||
searchForm: {
|
||||
drives: [],
|
||||
keyword: ''
|
||||
},
|
||||
availableDrives: ['C:'],
|
||||
searchResults: [],
|
||||
searching: false,
|
||||
searched: false,
|
||||
progress: 0,
|
||||
progressTimer: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAvailableDrives()
|
||||
},
|
||||
methods: {
|
||||
async fetchAvailableDrives() {
|
||||
const res = await getLocalDrives()
|
||||
if (res.code === 200) {
|
||||
this.availableDrives = res.data
|
||||
} else {
|
||||
this.$message.error('获取磁盘列表失败')
|
||||
return
|
||||
}
|
||||
},
|
||||
searchFiles() {
|
||||
if (!this.searchForm.drives.length) {
|
||||
this.$message.warning('请至少选择一个磁盘')
|
||||
return
|
||||
}
|
||||
if (!this.searchForm.keyword.trim()) {
|
||||
this.$message.warning('请输入关键字')
|
||||
return
|
||||
}
|
||||
|
||||
this.searching = true
|
||||
this.searched = false
|
||||
this.searchResults = []
|
||||
this.progress = 0
|
||||
|
||||
// 开始进度条动画
|
||||
this.startProgress()
|
||||
buildIndex(this.searchForm.drives).then((res) => {
|
||||
if (res.code === 200) {
|
||||
localFileSearch(this.searchForm.keyword).then((res) => {
|
||||
if (res.code === 200) {
|
||||
this.searchResults = res.data
|
||||
.filter(path => this.searchForm.drives.some(drive => path.startsWith(drive)))
|
||||
.map(path => ({
|
||||
name: path.split('\\').pop(), // 提取文件名
|
||||
path: path
|
||||
}))
|
||||
this.searching = false
|
||||
this.searched = true
|
||||
} else {
|
||||
this.$message.error('搜索失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
clearInterval(this.progressTimer)
|
||||
this.progress = 100
|
||||
})
|
||||
},
|
||||
startProgress() {
|
||||
if (this.progressTimer) {
|
||||
clearInterval(this.progressTimer)
|
||||
}
|
||||
|
||||
let timeElapsed = 0
|
||||
const totalTime = 60000 // 60秒
|
||||
|
||||
this.progressTimer = setInterval(() => {
|
||||
timeElapsed += 500 // 每0.5秒更新一次
|
||||
|
||||
// 非匀速增长:前快后慢
|
||||
const progressRatio = timeElapsed / totalTime
|
||||
this.progress = Math.min(99, Math.floor(100 * (1 - Math.pow(1 - progressRatio, 2))))
|
||||
|
||||
if (timeElapsed >= totalTime) {
|
||||
clearInterval(this.progressTimer)
|
||||
}
|
||||
}, 500)
|
||||
},
|
||||
openLocalDir(path) {
|
||||
openDir(path)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 清理定时器
|
||||
if (this.progressTimer) {
|
||||
clearInterval(this.progressTimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.local-search {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.results-area {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
margin-top: 15px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 20px;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -271,6 +271,60 @@ const setPassword = (password) => {
|
||||
return http.post("/config/setPassword", formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取磁盘列表
|
||||
* @returns
|
||||
*/
|
||||
const getLocalDrives = () => {
|
||||
return http.get("/localFile/getDrives");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建本地文件索引
|
||||
* @param {array} drives
|
||||
* @returns
|
||||
*/
|
||||
const buildIndex = (drives) => {
|
||||
return http.post('/localFile/buildIndex', drives);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找本地文件
|
||||
* @param {string} keyword
|
||||
* @returns
|
||||
*/
|
||||
const localFileSearch = (keyword) => {
|
||||
return http.get("/localFile/search", {
|
||||
params: {
|
||||
keyword
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开文件资源管理器
|
||||
* @param {string} dir
|
||||
* @returns
|
||||
*/
|
||||
const openDir = (dir) => {
|
||||
const formData = new FormData();
|
||||
formData.append('dir', dir);
|
||||
return http.post("/localFile/openDir", formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询文件树
|
||||
* @param {string} path
|
||||
* @param {boolean} showFiles
|
||||
* @param {boolean} showFolders
|
||||
* @returns
|
||||
*/
|
||||
const getFileTree = (path, showFiles, showFolders, maxDepth) => {
|
||||
return http.get("/localFile/getFileTree", {
|
||||
params: { path, showFiles, showFolders, maxDepth }
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
getUploadProgress,
|
||||
createMultipartUpload,
|
||||
@@ -291,5 +345,10 @@ export {
|
||||
setPassword,
|
||||
addSharedFile,
|
||||
deleteFile,
|
||||
getLocalDrives,
|
||||
buildIndex,
|
||||
localFileSearch,
|
||||
openDir,
|
||||
getFileTree,
|
||||
httpExtra
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="tab-underline"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="activeTab !== 'gallery' && activeTab !== 'shared' && !isWipTab" class="upload-content">
|
||||
<div v-if="activeTab !== 'gallery' && activeTab !== 'toolbox' && activeTab !== 'shared' && !isWipTab" class="upload-content">
|
||||
<upload-file :file-list.sync="fileList" :accept="allowedFormats" :max-size="maxUploadSize" width="100%"
|
||||
height="400px" :storage-type="activeTab" />
|
||||
</div>
|
||||
@@ -36,6 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<share-file v-else-if="activeTab === 'shared'" />
|
||||
<file-toolbox v-else-if="activeTab === 'toolbox'" />
|
||||
<file-gallery v-else />
|
||||
<settings-modal :show="showSettings" :allowed-formats="allowedFormats" :max-upload-size="maxUploadSize"
|
||||
@save="saveSettings" @close="closeSettings" />
|
||||
@@ -54,10 +55,11 @@ import StorageConfigModal from '@/components/StorageConfigModal/StorageConfigMod
|
||||
import AboutModal from '@/components/AboutModal/AboutModal.vue';
|
||||
import ShareFile from '@/components/ShareFile/ShareFile.vue';
|
||||
import PasswordConfig from '@/components/PasswordConfig/PasswordConfig.vue';
|
||||
import FileToolbox from '@/components/Toolbox/FileToolbox.vue';
|
||||
|
||||
export default {
|
||||
name: 'MainPage',
|
||||
components: { UploadFile, FileGallery, SettingsModal, StorageConfigModal, AboutModal, ShareFile, PasswordConfig },
|
||||
components: { UploadFile, FileGallery, SettingsModal, StorageConfigModal, AboutModal, ShareFile, PasswordConfig, FileToolbox },
|
||||
data() {
|
||||
return {
|
||||
fileList: [],
|
||||
@@ -87,6 +89,7 @@ export default {
|
||||
{ label: 'QiNiu', value: 'qiniu' },
|
||||
{ label: '共享文件', value: 'shared' },
|
||||
...(this.isAdmin ? [{ label: '已上传文件', value: 'gallery' }] : []),
|
||||
{ label: '工具箱', value: 'toolbox' },
|
||||
];
|
||||
},
|
||||
isWipTab() {
|
||||
|
||||
Reference in New Issue
Block a user