打造企业级在线文件管理器,实现后台在线文件管理,下载和上传处理,回收站

一个真正工业级的文件管理器,中的在于上传和下载功能,我们这次将对上传和下载模块进行“重火力”升级

💡 核心设计理念(上传与三种下载方式)

  1. 上传进度条:基于 Axios 的 onUploadProgress 事件,精准计算上传百分比,并在前端弹出友好的进度条对话框。同时在上传前先调接口校验是否存在同名文件,提供“覆盖/跳过”选择。

  2. 动态下载策略机制

    • 方式 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值