Vue通过file控件上传文件到Node服务器

news2025/1/12 6:19:14

请添加图片描述功能: 1.多文件同时上传、2.拖动上传、3.实时上传进度条、4.中断上传和删除文件、5.原生file控件的美化

搁置的功能: 上传文件夹、大文件切片上传、以及其他限制条件未处理

Node服务器的前置准备:

新建文件夹:		       file_upload_serve
  
初始化npm:		       npm init -y

安装工具:		       npm add express multer

nodemon工具:           npm install nodemon -g

axios:                 npm  install  axios  -s

Node运行版本:  18.17.1

修改package.json文件
 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
改为:监听app.js
 "scripts": {
    "dev": "nodemon ./app.js"
  },
启动: npm run dev

Node > file_upload_serve > app.js

按前置准备完成,其他无需更改,请求部分全在app.js
在这里插入图片描述

/*
 * @Description: 
 * @Last Date: Do not edit
 */
const express = require('express')
// post请求解析body
const bodyParser = require('body-parser')
// 上传工具库
const multer = require('multer')
const { writeFileSync } = require('fs')
const { resolve } = require('path')
const path = require('path')
const fs = require('fs')

const app = express()
app.use(bodyParser.json({limit: '10mb', extended: true}))
// 静态资源共享(下载需要)
app.use(express.static(path.join(__dirname, 'public')))
// const storage = multer.diskStorage({
//   destination: function (req, file, callback) {
//     // 第一个参数: errorMessage;  参数2: 目标,即下载到哪个文件夹下
//     callback(null, 'uploads/')
//   },
//   filename: function (req, file, callback) {
//     // 获取上传文件的后缀名
//     const ext = file.originalname.split('.')[1]
//     callback(null, Date.now() + '.' + ext)
//   }
// })
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
      cb(null, 'uploads/') // 分片存储目录
  },
  filename: (req, file, cb) => {
    const ext = file.originalname.split('.')[1]
    if(req.body.rename) {
      cb(null, Date.now() + '.' + ext) // 单文件名
    } else {
      cb(null, `${req.body.index}-${req.body.fileName}`) // 分片文件名

    }
  }
})

 
// 生成upload对象
const upload = multer({
  storage,
})

// 设置请求头
app.all('*', (req, res, next) => {
  // 允许所有不同源的地址访问
  res.header('Access-Control-Allow-Origin', '*');
  // 跨域允许的请求方式
  res.header('Access-Control-Allow-Methods', 'GET, POST');
  // x-ext: 获取文件的后缀名
  // res.header('Access-Control-Allow-Headers', 'Content-Type, x-ext');
  // res.header('Access-Control-Allow-Headers', 'Content-Type');
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, x-ext");
  if (req.method.toLowerCase() == 'options'){
    res.send(200);  //让options尝试请求快速结束
  } else {
    next()
  }

})


/* 上传方式1: multipart/form-data
 *
 * upload.single  单文件上传
 */
app.post('/file', upload.single('file'), (req, res) => {
    if(req.file){
      res.send('formData上传成功')
    } else {
      res.send('form-data上传失败')
    }
})

/* 上传方式2: base64
 *
 * upload.single  单文件上传
 */
app.post('/base64', (req, res) => {
  const { file, ext, fileName } = req.body
  const binaryData = Buffer.from(file, 'base64')
  if(!fileName) {
    writeFileSync(resolve(__dirname, 'uploads/' + Date.now() + '.' + ext), binaryData, 'binary')
  } else {
    writeFileSync(resolve(__dirname, 'uploads/' + fileName), binaryData, 'binary')
  }
  res.send('base64文件流上传成功')
})

/* 上传方式3: binary 二进制
 *
 * upload.single  单文件上传
 */
app.post('/binary', (req, res) => {
  const ext = req.headers['x-ext']
  const buffers = []
   req.on('data', chunk => {
    buffers.push(chunk)
   }).on('end', () => {
    const binaryData = Buffer.concat(buffers)
    writeFileSync(resolve(__dirname, 'uploads/' + Date.now() + '.' + ext), binaryData, 'binary')
    res.send('二进制流上传成功')
   })
})
/* 多文件上传: formData
 *
 * upload.array('formData中的字段名', 最大上传数量): 
 */
app.post('/files', upload.array('files', 4), (req, res) => {
  console.log(req.files)
  if(req.files){
    res.send('多文件formData上传成功')
  } else {
    res.send('多文件formData上传失败')
  }
})
/* 文件下载
 * __dirname: 代表当前文件<app.js>所在的文件路径
 */
app.get('/download', (req, res) => {
  try{
    // 下载路径: __dirname 拼接 第二个参数的路径
    const filePath = path.join(__dirname, '/public/download/1731726859151.txt')
    res.download(filePath)
  }catch(e){
    console.log(e)
  }
  
})


app.post('/merge', async (req, res) => {

  const uploadPath = '/uploads'
  let files = fs.readdirSync(path.join(process.cwd(), uploadPath)) // 获取所有的分片数据
  console.log(files)
  console.log(req.body.fileName)
  files = files.sort((a, b) => a.split('-')[0] - b.split('-')[0]) // 将分片按照文件名进行排序
  const writePath = path.join(process.cwd(), uploadPath, `${req.body.fileName}`) // 生成新的文件路径
  files.forEach((item) => {
      fs.appendFileSync(writePath, fs.readFileSync(path.join(process.cwd(), uploadPath, item))) // 读取分片信息,追加到新文件路径尾部
      fs.unlinkSync(path.join(process.cwd(), uploadPath, item)) // 将读取过的分片进行删除
  })
 
  res.send('ok')
})


app.listen(8888, () => {console.log("链接成功")})

客户端

 
<!--
 * @Description:  功能: 1.多文件同时上传、2.拖动上传、3.实时上传进度条、
 *                      4.中断上传和删除文件、5.原生file控件的美化
 * 搁置的功能: 上传文件夹、大文件切片上传、以及其他限制条件未处理
 * @Last Date: Do not edit
-->
<template>
  <div class="container">
    <header>
      <div
        class="box"
        @drop="handleClick"
        @dragenter="handleClick"
        @dragover="handleClick"
        @dragleave="handleClick"
      >
        <div class="box-font">
          <div>
            <span style="display: flex; align-items: center"
              ><i class="el-icon-upload"> </i>
              <p>将目录或多个文件拖拽到此进行扫描</p></span
            >
          </div>

          <div>
            <span>支持的文件类型: .JPG.JPEG.BMP.PNG.GIF.ZIP</span>
          </div>
          <div><span>每个文件允许的最大尺寸: 1M</span></div>
        </div>
      </div>
    </header>
    <main>
      <div class="main-choose-files-btn">
        <div class="file-box">
          <input type="button" class="btn" value="选择文件" />
          <input
            type="file"
            class="file"
            @change="previewMoreFilesByFormData"
            multiple
          />
        </div>

        <div class="file-box">
          <input type="button" class="btn" value="选择文件夹" />
          <input
            type="file"
            class="file"
            @change="previewMoreFilesByFormData"
            multiple
          />
        </div>
      </div>
      <div>
        <el-table :data="tableData" stripe style="width: 85%">
          <!-- <el-table-column
            v-for="item in tableColumn"
            :key="item.prop"
            :prop="item.prop"
            :label="item.label"
          ></el-table-column> -->

          <el-table-column
            prop="name"
            label="文件名"
            width="240"
            fixed
          ></el-table-column>
          <el-table-column prop="type" label="类型"></el-table-column>
          <el-table-column prop="size" label="大小"></el-table-column>
          <el-table-column prop="state" label="状态">
            <!-- 当template中有多个元素需要切换时,需要在最外层使用div将所有元素包裹住 -->
            <!-- slot-scope="scope" 必须加,否则数据不是响应式的 -->
            <template slot-scope="scope">
              <div>
                <div
                  v-show="
                    scope.row.progressPercent > 0 &&
                    scope.row.progressPercent < 100
                  "
                >
                  <el-progress
                    :text-inside="true"
                    :stroke-width="15"
                    :percentage="scope.row.progressPercent"
                  />
                </div>

                <div
                  v-show="scope.row.progressPercent < 1"
                  slot="reference"
                  class="name-wrapper"
                >
                  <el-tag size="medium"> 待上传 </el-tag>
                </div>
                <div
                  v-show="scope.row.progressPercent === 100"
                  slot="reference"
                  class="name-wrapper"
                >
                  <el-tag size="medium"> 已上传 </el-tag>
                </div>
              </div>
            </template>
          </el-table-column>
          <el-table-column label="操作">
            <template slot-scope="scope">
              <i class="el-icon-delete" @click="deleteFile(scope.row)"></i>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </main>
    <footer>
      <el-row>
        <el-button class="foot-btn" size="mini">
          <span>文件数量: {{ tableData.length }}</span>
        </el-button>
        <el-button class="foot-btn" type="success" plain size="mini">
          成功数量: {{ successCount }}
        </el-button>
        <el-button class="foot-btn" size="mini">
          <span>总大小: {{ countSize }} MB</span>
        </el-button>
      </el-row>
      <el-row class="upload-btn">
        <el-button
          type="primary"
          :disabled="uploadDisabled"
          @click="handelUploadMoreFile"
          >开始上传</el-button
        >
      </el-row>
    </footer>
  </div>
</template>

<script>
import axios from "axios"
export default {
  data() {
    return {
      ext: undefined, // 文件后缀名
      tableData: [],
      tableColumn: [
        { prop: "name", label: "文件名" },
        { prop: "type", label: "类型" },
        { prop: "size", label: "大小" },
        { prop: "progressPercent", label: "状态" },
        { prop: "option", label: "操作" },
      ],
      filesNumber: 1, // 列表文件总条数
      successCount: 0, // 上传成功条数
      cancelTokens: [], // 存储每个请求的取消方法
    }
  },
  mounted() {
    // 阻止事件冒泡,防止在拖拽后意外打开新标签页
    document.body.ondrop = function (event) {
      event.preventDefault()
      event.stopPropagation()
    }
  },
  computed: {
    getByte() {
      return this.tableData.reduce((a, b) => {
        return a + b.size
      }, 0)
    },
    // 文件总大小
    countSize() {
      return (this.getByte / 1048576).toFixed(2)
    },
    // 是否禁用"上传按钮"
    uploadDisabled() {
      return this.tableData.length > 0 ? false : true
    },
  },
  methods: {
    // 读取多个文件
    previewMoreFilesByFormData(e, drop) {
      let files
      if (!drop) {
        files = e.target.files
      } else {
        files = e
      }

      // 获取文件后缀名
      this.ext = files[0].name.split(".")[1]
      if (!files) return

      var i = 0
      var _this = this
      var funcs = function () {
        if (files[i]) {
          var reader = new FileReader()

          reader.onload = function (e) {
            const uint8Array = new Uint8Array(e.target.result)
            const str = uint8Array.reduce((prev, byte) => {
              prev += String.fromCharCode(byte)
              return prev
            }, "")

            let now = new Date()
            // 由于JS执行速度很快,极大可能会得到一样的时间戳,故将timestamp加上下标
            // timestamp的作用是在将来删除文件时,作为唯一id对比删除
            let timestamp = now.getTime()
            // 将预览的文件中数据转换到table中
            _this.tableData.push({
              timestamp: timestamp + i,
              name: files[i].name,
              type: files[i].type,
              size: files[i].size,
              progressPercent: 0,
              dataBase64: btoa(str),
            })
            // progressPercent  上传进度条
            i++
            funcs() // onload为异步调用
          }
          reader.readAsArrayBuffer(files[i])
        }
      }
      funcs()
    },

    /** 删除上传文件
     * 不能通过数组下标去删。删除再添加新文件时,下标会重复
     * @param row(行数据)
     */
    deleteFile(row) {
      // 删除文件
      this.tableData = this.tableData.filter(
        (item) => item.timestamp !== row.timestamp
      )

      // 中断请求
      const requestToCancel = this.cancelTokens.find(
        (token) => token.requestId === row.requestId
      )
      if (requestToCancel && requestToCancel.cancel) {
        requestToCancel.cancel("Request was canceled by the user.")
      }
    },

    // 上传文件
    handelUploadMoreFile() {
      let _this = this
      const CancelToken = axios.CancelToken
      const List = []

      for (let i = 0; i < this.tableData.length; i++) {
        // console.log(source.cancel)
        const ext = this.ext // 文件后缀名
        // 为每个请求创建一个新的取消令牌源
        const cancelTokenSource = CancelToken.source()
        this.cancelTokens.push({
          requestId: i,
          cancel: cancelTokenSource.cancel,
        })
        // 给tableData设置"requestId",将来取消请求通过比对id对应到具体的请求
        this.tableData[i].requestId = i
        var a = axios({
          url: "http://localhost:8888/base64",
          method: "post",
          cancelToken: cancelTokenSource.token,
          data: {
            ext,
            fileName: this.tableData[i].name,
            file: this.tableData[i].dataBase64,
          },
          onUploadProgress: (progressEvent) => {
            /**  上传进度条
             *   progressEvent.loaded: 已上传文件大小
             *   progressEvent.total:  被上传文件的总大小
             */
            _this.tableData[i].progressPercent =
              (progressEvent.loaded / progressEvent.total) * 100
          },
        })
          .then((res) => {
            // this.$message({
            //     message: '文件上传成功',
            //     type: 'success'
            //   })
            // console.log(res)
          })
          .catch((error) => {
            if (axios.isCancel(error)) {
              console.log(error.message)
            } else {
              console.log(error.message)
            }
          })
        List.push(a)
      }
      // 合并异步上传
      Promise.all(List)
        .then((res) => {})
        .catch((err) => {})
    },
    // 处理鼠标拖放事件
    handleClick(e) {
      if (e.type == "dragenter") {
        // this.className = "drag_hover"
      }
      if (e.type == "dragleave") {
        // this.className = ""
      }
      if (e.type == "drop") {
        var files = e.dataTransfer.files
        this.className = ""
        if (files.length != 0) {
          this.previewMoreFilesByFormData(files, "drop")
        }
      }
      if (e.type == "dragover") {
        // e.dataTransfer.dragEffect = "copy"
      }
    },
  },
}
</script>

<style lang="scss">
body,
html {
  list-style: none;
  padding: 0;
  margin: 0;
}
.container {
  width: 85%;
  margin: 25px auto;
  .box {
    width: 85%;
    height: 300px;
    border-style: dashed; // border虚线
    border-width: 1px;
    margin-bottom: 20px;

    display: flex; /* 启用 Flexbox */
    justify-content: center; /* 水平居中 */
    align-items: center; /* 垂直居中 */

    .box-font {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 30px;
      span {
        display: block;
      }
    }
  }
  main {
    .main-choose-files-btn {
      display: flex;
      gap: 100px;
      height: 44px;
    }
  }
  footer {
    margin-top: 20px;
    .upload-btn {
      margin-top: 10px;
    }
  }
}
// 对原生file控件优化
.btn,
.file {
  @extend .merge-input;
}
.merge-input {
  // display: block;
  position: absolute;
  width: 75px;
  height: 35px;

  color: #fff;
  border-radius: 4px;
  border-color: #409eff;
}
.btn {
  z-index: 2;
  background: #409eff; //  #66b1ff    409eff
  pointer-events: none; /* 让事件传递到下一层,即: btn的层级比file高,但btn能触发file的事件 */
}
.file {
  z-index: 1;
}
// el-table表头样式修改
.el-table th {
  font-size: 13px;
  font-weight: 700;
}

.el-table .el-table__header th,
.el-table .el-table__header tr,
.el-table .el-table__header td {
  background: #f5f8fd;
}

.el-icon-upload {
  font-size: 35px;
}
</style>


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2244788.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Security使用基本认证(Basic Auth)保护REST API

基本认证概述 基本认证&#xff08;Basic Auth&#xff09;是保护REST API最简单的方式之一。它通过在HTTP请求头中携带Base64编码过的用户名和密码来进行身份验证。由于基本认证不使用cookie&#xff0c;因此没有会话或用户登出的概念&#xff0c;这意味着每次请求都必须包含…

[大数据] Iceberg

G:\Bigdata\25.iceberg 第3章 与 Hive集成 3.1 环境准备 1)Hive与Iceberg的版本对应关系如下 Hive 版本 官方推荐Hive版本 Iceberg 版本 2.x 2.3.8 0.8.0-incubating – 1.1.0 3.x 3.1.2 0.10.0 – 1.1.0 Iceberg与Hive 2和Hive 3.1.2/3的集成,支持以下特性: 创建表删除表…

JMeter监听器与压测监控之Grafana

Grafana 是一个开源的度量分析和可视化套件&#xff0c;通常用于监控和观察系统和应用的性能。本文将指导你如何在 Kali Linux 上使用 Docker 来部署 Grafana 性能监控平台。 前提条件 Kali Linux&#xff1a;确保你已经安装了 Kali Linux。Docker&#xff1a;确保你的系统已…

C/C++ 优化,strlen 示例

目录 C/C optimization, the strlen examplehttps://hallowed-blinker-3ca.notion.site/C-C-optimization-the-strlen-example-108719425da080338d94c79add2bb372 揭开优化的神秘面纱... 让我们来谈谈 CPU 等等&#xff0c;SIMD 是什么&#xff1f; 为什么 strlen 是一个很…

【Linux学习】【Ubuntu入门】1-8 ubuntu下压缩与解压缩

1.Linux系统下常用的压缩格式 常用的压缩扩展名&#xff1a;.tar、.tar.bz2、.tar.gz 2.Windows下7ZIP软件安装 Linux系统下很多文件是.bz2&#xff0c;.gz结尾的压缩文件。 3.Linux系统下gzip压缩工具 gzip工具负责压缩和解压缩.gz格式的压缩包。 gzip对单个文件进行…

【Linux网络编程】简单的UDP套接字

目录 一&#xff0c;socket编程的相关说明 1-1&#xff0c;sockaddr结构体 1-2&#xff0c;Socket API 二&#xff0c;基于Udp协议的简单通信 三&#xff0c;UDP套接字的应用 3-1&#xff0c;实现英译汉字典 一&#xff0c;socket编程的相关说明 Socket编程是一种网络通信…

【工控】线扫相机小结 第三篇

海康软件更新 目前使用的是 MVS_STD_4.3.2_240705.exe &#xff0c;最新的已经到4.4了。 一个大的变动 在上一篇中我们提到一个问题&#xff1a; 需要注意的是&#xff0c;我们必须先设置 TriggerSelector 是 “FrameBurstStart” 还是 “LineStart” 再设置TriggerMode 是 …

Java基础知识(五)

文章目录 ObjectObject 类的常见方法有哪些&#xff1f; 和 equals() 的区别hashCode() 有什么用&#xff1f;为什么要有 hashCode&#xff1f;为什么重写 equals() 时必须重写 hashCode() 方法&#xff1f; 参考链接 Object Object 类的常见方法有哪些&#xff1f; Object 类…

[高阶数据结构(一)]并查集详解

1.前言 本系列会带大家走进高阶数据结构的学习, 其中包括并查集,图论, LRU cache, B树, B树, B*树, 跳表. 其中, 图论中讲解的时间最长, 包括邻接表, 邻接矩阵, 广度优先遍历, 深度优先遍历, 最小生成树中的kruskal算法以及prim算法&#xff1b;最短路径中的dijkstra算法, bell…

应聘美容师要注意什么?博弈美业收银系统/管理系统/拓客系统分享建议

随着美容行业的不断发展&#xff0c;成为一名优秀的美容师需要具备一系列重要的技能和品质。无论是在面试过程中还是在实际工作中&#xff0c;以下建议将帮助你在应聘美容师职位时脱颖而出&#xff1a; ▶ 专业技能和资格 首先&#xff0c;确保你具备所需的专业技能和资格。这…

el-cascader 使用笔记

1.效果 2.官网 https://element.eleme.cn/#/zh-CN/component/cascader 3.动态加载&#xff08;官网&#xff09; <el-cascader :props"props"></el-cascader><script>let id 0;export default {data() {return {props: {lazy: true,lazyLoad (…

vmWare虚拟环境centos7安装Hadoop 伪分布式实践

背景&#xff1a;近期在研发大数据中台&#xff0c;需要研究Hadoop hive 的各种特性&#xff0c;需要搭建一个Hadoop的虚拟环境&#xff0c;本来想着使用dock &#xff0c;但突然发现docker 公共仓库的镜像 被XX 了&#xff0c;无奈重新使用vm 搭建虚拟机。 大概经历了6个小时完…

Redis基本的全局命令

在学习redis基本的全局命令之前呢&#xff0c;我们必须先进入redis-cli客户端才行。 如图&#xff1a; get和set get和set是redis两个最核心的命令。 get&#xff1a;根据key来获取value。 set&#xff1a;把key和value存储进去。 如set命令如图&#xff1a; 对于上述图中&…

Redis五大基本类型——List列表命令详解(命令用法详解+思维导图详解)

目录 一、List列表类型介绍 二、常见命令 1、LPUSH 2、LPUSHX 3、RPUSH 4、RPUSHX 5、LRANGE 6、LPOP 7、RPOP 8、LREM 9、LSET 10、LINDEX 11、LINSERT 12、LLEN 13、阻塞版本命令 BLPOP BRPOP 三、命令小结 相关内容&#xff1a; Redis五大基本类型——Ha…

一文详解哋它亢模块的安装与使用

如何安装哋它亢模块 哋它亢模块是扩展哋它亢功能的关键工具&#xff0c;它们涵盖了从数据分析到机器学习的各种应用场景。通过安装和使用这些模块&#xff0c;你可以轻松完成复杂的任务&#xff0c;大幅提升开发效率。哋它亢是一门易于学习且功能强大的编程语言&#xff0c;以…

C#中的二维数组的应用:探索物理含义与数据结构的奇妙融合

在C#编程中&#xff0c;二维数组&#xff08;或矩阵&#xff09;是一种重要的数据结构&#xff0c;它不仅能够高效地存储和组织数据&#xff0c;还能通过其行、列和交叉点&#xff08;备注&#xff1a;此处相交处通常称为“元素”或“单元格”&#xff0c;代表二维数组中的一个…

论文阅读——Intrusion detection systems using longshort‑term memory (LSTM)

一.基本信息 论文名称&#xff1a;Intrusion detection systems using longshort‑term memory (LSTM) 中文翻译&#xff1a;基于长短期记忆(LSTM)的入侵检测系统 DOI&#xff1a;10.1186/s40537-021-00448-4 作者&#xff1a;FatimaEzzahra Laghrissi1* , Samira Douzi2*, Kha…

【行之有效】实证软件工程研究方法

【行之有效】实证软件工程研究方法 一、实证研究二、实证软件工程2.1 系统化文献评价2.2 调查研究2.2.1 数据收集2.2.2 抽样 2.3 案例研究2.3 实证研究效度 一、实证研究 实证研究&#xff08;Empirical Research&#xff09;方法是一种与规范研究&#xff08;Normative Resea…

大数据挖掘期末复习

大数据挖掘 数据挖掘 数据挖掘定义 技术层面&#xff1a; 数据挖掘就是从大量的、不完全的、有噪声的、模糊的、随机的实际应用数据中&#xff0c;提取隐含在其中、人们事先不知道的、但又潜在有用的信息的过程。 数据准备环节 数据选择 质量分析 数据预处理 数据仓库 …

河道水位流量一体化自动监测系统:航运安全的护航使者

在广袤的水域世界中&#xff0c;航运安全始终是至关重要的课题。而河道水位流量一体化自动监测系统的出现&#xff0c;如同一位强大的护航使者&#xff0c;为航运事业的稳定发展提供了坚实的保障。 水位传感器&#xff1a;负责实时监测河道的水位变化。这些传感器通常采用先进的…