feat:新增文件工具箱

- 本地查找
- 文件树状图
- Diff
- 文件加密
- 批量修改文件名
- 文件格式转换
This commit is contained in:
czhqwer
2025-04-01 23:12:24 +08:00
parent eabdb649f1
commit ec6caafed9
15 changed files with 1027 additions and 47 deletions

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View 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; // 子节点
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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"

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
};

View File

@@ -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() {