一个真正工业级的文件管理器,中的在于上传和下载功能,我们这次将对上传和下载模块进行“重火力”升级。
💡 核心设计理念(上传与三种下载方式)
-
上传进度条:基于 Axios 的 onUploadProgress 事件,精准计算上传百分比,并在前端弹出友好的进度条对话框。同时在上传前先调接口校验是否存在同名文件,提供“覆盖/跳过”选择。
-
动态下载策略机制:
-
方式 A: Web Worker 下载 (推荐用于中小文件 < 100MB):利用浏览器后台独立线程处理网络 I/O,并且能精确回传进度信息给 UI 层渲染进度条,完全不阻塞主线程。缺点是需要将整个文件读入内存(Blob),超大文件可能导致浏览器崩溃。
-
方式 B: Service Worker 下载 (推荐用于中大文件 100MB ~ 2GB):Service Worker 可以拦截浏览器的请求并通过 Streams API 边走边刷入磁盘(触发浏览器的原生下载),同时通过 postMessage 向主线程汇报进度。这既能展示 UI 进度条,又不需要把几 GB 的文件全部塞进内存。
-
方式 C: 传统普通下载 (兜底方案,大文件或不支持 Worker 时使用):直接通过通过创建隐藏的 <a> 标签打开下载链接,将下载任务彻底交给浏览器的原生下载管理器来硬扛,无法获取前端 UI 进度条,但最稳定。
-
以下为您送上这套终极版的完整前后端代码实现方案:
一、 核心前端补充组件 (单独存放在静态目录下)
为了实现 Worker 功能,请在您的 Vue 项目 public 目录下创建这两个 JS 文件。
1. public/downloadWorker.js (Web Worker 脚本)
负责后台纯内存下载并汇报进度。
// public/downloadWorker.js
self.onmessage = function (e) {
const { url, filename } = e.data;
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob'; // 关键:指定返回 Blob 类型
xhr.onprogress = function (event) {
if (event.lengthComputable) {
const percent = Math.floor((event.loaded / event.total) * 100);
// 向主线程汇报进度
self.postMessage({ type: 'progress', percent: percent, loaded: event.loaded, total: event.total });
}
};
xhr.onload = function () {
if (this.status === 200) {
self.postMessage({ type: 'success', blob: this.response, filename: filename });
} else {
self.postMessage({ type: 'error', error: 'Download failed with status: ' + this.status });
}
};
xhr.onerror = function () {
self.postMessage({ type: 'error', error: 'Network error occurred' });
};
xhr.send();
};
2. public/service-worker.js (Service Worker 脚本)
拦截特殊 URL 请求,实现边下载边流式缓存并汇报进度。(为简化演示,此处以拦截特殊参数并通过流的形式传递为主逻辑样例)
// public/service-worker.js
self.addEventListener('install', (event) => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// 拦截特定的下载请求
if (url.searchParams.has('sw_download')) {
event.respondWith(handleDownloadStream(event));
}
});
async function handleDownloadStream(event) {
const request = event.request;
const targetUrl = request.url.replace('&sw_download=true', '').replace('?sw_download=true', '');
const response = await fetch(targetUrl);
const contentLength = response.headers.get('content-length');
const total = parseInt(contentLength, 10);
let loaded = 0;
const stream = new ReadableStream({
async start(controller) {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
loaded += value.length;
// 计算并广播进度给所有打开的页面 (Clients)
if (total) {
const percent = Math.floor((loaded / total) * 100);
self.clients.matchAll().then(clients => {
clients.forEach(client => client.postMessage({
type: 'sw_progress', url: targetUrl, percent, loaded, total
}));
});
}
controller.enqueue(value);
}
controller.close();
self.clients.matchAll().then(clients => {
clients.forEach(client => client.postMessage({ type: 'sw_success', url: targetUrl }));
});
}
});
return new Response(stream, {
headers: response.headers
});
}
二、 前端 Vue 主界面代码 (FileManager.vue)
这部分代码整合了所有功能:新建、删除(包括丢入回收站)、批量操作、上传排重与进度条,以及综合判定的3种智能下载模式切换。
<template>
<div class="file-manager-container">
<!-- 顶部操作栏 -->
<div class="action-bar">
<el-button-group>
<el-button type="primary" size="small" icon="el-icon-back" @click="goBack" :disabled="historyIndex <= 0" class="hidden-xs-only">后退</el-button>
<el-button type="primary" size="small" icon="el-icon-right" @click="goForward" :disabled="historyIndex >= history.length - 1" class="hidden-xs-only">前进</el-button>
<el-button type="primary" size="small" icon="el-icon-top" @click="goUpDir" :disabled="currentPath === '/'">上一级</el-button>
</el-button-group>
<!-- 地址栏 -->
<el-input
v-model="pathInput"
size="small"
placeholder="输入路径按回车跳转 (如 /usr/logs)"
@keyup.enter.native="jumpToPath"
style="width: 250px; margin-left: 10px;"
class="hidden-xs-only"
>
<template slot="prepend">路径</template>
</el-input>
<!-- 新建操作组 -->
<el-dropdown @command="handleNewCommand" style="margin-left: 10px;">
<el-button type="success" size="small">
<i class="el-icon-folder-add"></i> 新建 <i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="folder">新建文件夹</el-dropdown-item>
<el-dropdown-item command="file">新建空文件</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 文件上传 -->
<el-upload
class="upload-demo"
style="display: inline-block; margin-left: 10px;"
action=""
:show-file-list="false"
:http-request="customUploadRequest"
>
<el-button size="small" type="primary"><i class="el-icon-upload"></i> 上传文件</el-button>
</el-upload>
<el-button @click="openRecycleBin" size="small" type="warning" style="margin-left: 10px;">
<i class="el-icon-delete-solid"></i> 回收站
</el-button>
<el-input v-model="searchKeyword" size="small" placeholder="搜索文件..." style="width: 200px; margin-left: auto;" @keyup.enter.native="searchFiles">
<el-button slot="append" icon="el-icon-search" @click="searchFiles"></el-button>
</el-input>
</div>
<!-- 批量操作栏 -->
<div class="batch-action-bar" v-if="selectedFiles.length > 0">
<span style="font-size: 13px; margin-right: 15px;">已选择 {
{ selectedFiles.length }} 项</span>
<el-button size="mini" type="primary" @click="clipFiles('copy')">复制</el-button>
<el-button size="mini" type="warning" @click="clipFiles('cut')">剪切</el-button>
<el-button size="mini" type="danger" @click="batchDelete">批量删除</el-button>
</div>
<!-- 剪贴板粘贴提示栏 -->
<div class="clipboard-bar" v-if="clipboard.length > 0">
<el-alert
:title="`剪贴板中有 ${clipboard.length} 个项目等待 ${clipboardAction === 'copy' ? '复制' : '移动'} 到此处 `"
type="info"
show-icon
:closable="false"
>
<el-button size="mini" type="success" @click="pasteFiles">立即粘贴</el-button>
<el-button size="mini" type="text" @click="clearClipboard">取消</el-button>
</el-alert>
</div>
<!-- 文件列表表格 -->
<el-table
:data="fileList"
v-loading="loading"
style="width: 100%; margin-top: 10px;"
height="500"
stripe
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="name" label="名称" sortable min-width="200">
<template slot-scope="scope">
<div class="file-name-cell" @click="handleFileClick(scope.row)">
<i v-if="scope.row.directory" class="el-icon-folder" style="color: #E6A23C; margin-right: 8px;"></i>
<i v-else class="el-icon-document" style="color: #909399; margin-right: 8px;"></i>
<span class="file-name">{
{ scope.row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column


4532

被折叠的 条评论
为什么被折叠?



