Skip to content

文件上传(网速、时间、进度)

在一些特殊的情况下,上传文件时,需要实时显示上传进度、上传速度、上传时间等信息,这个不算简单的,上传进度不难,有自带的监听,但是上传速度和上传时间就有点麻烦了,需要自己计算。

计算方法

计算当前上传速度 (已上传字节数 / 已用时间)

剩余时间 (剩余字节数(文件大小的总数 - 已上传字节数) / 当前速度)

同时可能需要做一些优化,比如>60秒显示分,<60秒显示秒,还可能需要显示网速。其实还有其他的突然中断、网速的剧烈变化都需要结合实际情况而定。

前端代码

vue
<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <div v-if="uploading" class="status">
      已上传 {{ progress }}%,剩余时间:{{ formattedRemainingTime }},速度:{{ uploadSpeed }}
    </div>
    <div v-else class="status">等待上传...</div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import axios from 'axios'

// 响应式数据
const file = ref(null)
const uploading = ref(false)
const loaded = ref(0)
const total = ref(0)
const progress = ref(0)
const remainingTime = ref(0) // 秒
const speed = ref(0) // 字节/秒

// 格式化显示
const formattedRemainingTime = ref('0秒')
const uploadSpeed = ref('0 KB/s')

// 文件选择事件
function handleFileChange(event) {
  const selectedFile = event.target.files[0]
  if (!selectedFile) return

  file.value = selectedFile
  startUpload()
}

// 启动上传
function startUpload() {
  uploading.value = true
  loaded.value = 0
  total.value = file.value.size
  const startTime = Date.now()

  const formData = new FormData()
  formData.append('file', file.value)

  // 使用 axios 发起上传请求
  axios
    .post('http://localhost:3000/api/upload', formData, {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
      onUploadProgress: (progressEvent) => {
        if (progressEvent.lengthComputable) {
          loaded.value = progressEvent.loaded
          total.value = progressEvent.total
          progress.value = Math.round((loaded.value / total.value) * 100)

          const elapsedTime = (Date.now() - startTime) / 1000 // 秒
          speed.value = loaded.value / elapsedTime || 0

          // 计算剩余时间(秒)
          const remaining = (total.value - loaded.value) / speed.value || 0
          remainingTime.value = Math.max(0, Math.floor(remaining))

          // 格式化剩余时间
          const minutes = Math.floor(remainingTime.value / 60)
          const seconds = remainingTime.value % 60
          if (minutes > 0) {
            formattedRemainingTime.value = `${minutes}分钟${seconds}秒`
          } else {
            formattedRemainingTime.value = `${seconds}秒`
          }

          // 格式化上传速度(KB/s 或 MB/s)
          if (speed.value < 1024) {
            uploadSpeed.value = `${speed.value.toFixed(2)} B/s`
          } else if (speed.value < 1024 * 1024) {
            uploadSpeed.value = `${(speed.value / 1024).toFixed(2)} KB/s`
          } else {
            uploadSpeed.value = `${(speed.value / (1024 * 1024)).toFixed(2)} MB/s`
          }
        }
      },
    })
    .then(() => {
      uploading.value = false
      formattedRemainingTime.value = '0秒'
      uploadSpeed.value = '0 KB/s'
      alert('上传完成!')
    })
    .catch((error) => {
      uploading.value = false
      formattedRemainingTime.value = '上传失败'
      uploadSpeed.value = 'N/A'
      alert('上传失败,请检查网络')
      console.error('Upload error:', error)
    })
}
</script>

<style scoped>
.status {
  margin-top: 10px;
  font-size: 14px;
  color: #333;
}
</style>
<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <div v-if="uploading" class="status">
      已上传 {{ progress }}%,剩余时间:{{ formattedRemainingTime }},速度:{{ uploadSpeed }}
    </div>
    <div v-else class="status">等待上传...</div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import axios from 'axios'

// 响应式数据
const file = ref(null)
const uploading = ref(false)
const loaded = ref(0)
const total = ref(0)
const progress = ref(0)
const remainingTime = ref(0) // 秒
const speed = ref(0) // 字节/秒

// 格式化显示
const formattedRemainingTime = ref('0秒')
const uploadSpeed = ref('0 KB/s')

// 文件选择事件
function handleFileChange(event) {
  const selectedFile = event.target.files[0]
  if (!selectedFile) return

  file.value = selectedFile
  startUpload()
}

// 启动上传
function startUpload() {
  uploading.value = true
  loaded.value = 0
  total.value = file.value.size
  const startTime = Date.now()

  const formData = new FormData()
  formData.append('file', file.value)

  // 使用 axios 发起上传请求
  axios
    .post('http://localhost:3000/api/upload', formData, {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
      onUploadProgress: (progressEvent) => {
        if (progressEvent.lengthComputable) {
          loaded.value = progressEvent.loaded
          total.value = progressEvent.total
          progress.value = Math.round((loaded.value / total.value) * 100)

          const elapsedTime = (Date.now() - startTime) / 1000 // 秒
          speed.value = loaded.value / elapsedTime || 0

          // 计算剩余时间(秒)
          const remaining = (total.value - loaded.value) / speed.value || 0
          remainingTime.value = Math.max(0, Math.floor(remaining))

          // 格式化剩余时间
          const minutes = Math.floor(remainingTime.value / 60)
          const seconds = remainingTime.value % 60
          if (minutes > 0) {
            formattedRemainingTime.value = `${minutes}分钟${seconds}秒`
          } else {
            formattedRemainingTime.value = `${seconds}秒`
          }

          // 格式化上传速度(KB/s 或 MB/s)
          if (speed.value < 1024) {
            uploadSpeed.value = `${speed.value.toFixed(2)} B/s`
          } else if (speed.value < 1024 * 1024) {
            uploadSpeed.value = `${(speed.value / 1024).toFixed(2)} KB/s`
          } else {
            uploadSpeed.value = `${(speed.value / (1024 * 1024)).toFixed(2)} MB/s`
          }
        }
      },
    })
    .then(() => {
      uploading.value = false
      formattedRemainingTime.value = '0秒'
      uploadSpeed.value = '0 KB/s'
      alert('上传完成!')
    })
    .catch((error) => {
      uploading.value = false
      formattedRemainingTime.value = '上传失败'
      uploadSpeed.value = 'N/A'
      alert('上传失败,请检查网络')
      console.error('Upload error:', error)
    })
}
</script>

<style scoped>
.status {
  margin-top: 10px;
  font-size: 14px;
  color: #333;
}
</style>

后端代码

后端就用nodejs,简单、方便、快捷

js
const express = require('express')
const multer = require('multer')
const cors = require('cors')
const path = require('path')
const fs = require('fs')

// 创建 Express 应用
const app = express()

// 配置 CORS 支持跨域请求
app.use(cors())

// 确保 uploads 目录存在
const uploadDir = path.join(__dirname, 'uploads')
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir)
}

// 配置 multer 存储
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, uploadDir) // 文件存储路径
  },
  filename: function (req, file, cb) {
    // 保留原始文件名(或自定义)
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
    const ext = path.extname(file.originalname)
    const filename = file.fieldname + '-' + uniqueSuffix + ext;
    req.uploadingFileName = filename; // 关键:存储文件名到请求对象
    cb(null, file.fieldname  + '-' + uniqueSuffix + ext);

  }
})

// 创建 multer 实例
const upload = multer({ 
  storage
})

// 处理上传接口
app.post('/api/upload', upload.single('file'), (req, res) => {
  try {
    // req.file 是上传的文件信息
    console.log('文件已上传:', req.file)

    // 返回成功响应
    res.status(200).json({
      message: '上传成功',
      file: {
        filename: req.file.filename,
        path: req.file.path,
        size: req.file.size
      }
    })
  } catch (error) {
    console.error('上传失败:', error)
    res.status(500).json({ message: '上传失败', error: error.message })
  }
})

// 启动服务器
const PORT = 3000
app.listen(PORT, () => {
  console.log(`Server is running at http://localhost:${PORT}`)
})
const express = require('express')
const multer = require('multer')
const cors = require('cors')
const path = require('path')
const fs = require('fs')

// 创建 Express 应用
const app = express()

// 配置 CORS 支持跨域请求
app.use(cors())

// 确保 uploads 目录存在
const uploadDir = path.join(__dirname, 'uploads')
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir)
}

// 配置 multer 存储
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, uploadDir) // 文件存储路径
  },
  filename: function (req, file, cb) {
    // 保留原始文件名(或自定义)
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
    const ext = path.extname(file.originalname)
    const filename = file.fieldname + '-' + uniqueSuffix + ext;
    req.uploadingFileName = filename; // 关键:存储文件名到请求对象
    cb(null, file.fieldname  + '-' + uniqueSuffix + ext);

  }
})

// 创建 multer 实例
const upload = multer({ 
  storage
})

// 处理上传接口
app.post('/api/upload', upload.single('file'), (req, res) => {
  try {
    // req.file 是上传的文件信息
    console.log('文件已上传:', req.file)

    // 返回成功响应
    res.status(200).json({
      message: '上传成功',
      file: {
        filename: req.file.filename,
        path: req.file.path,
        size: req.file.size
      }
    })
  } catch (error) {
    console.error('上传失败:', error)
    res.status(500).json({ message: '上传失败', error: error.message })
  }
})

// 启动服务器
const PORT = 3000
app.listen(PORT, () => {
  console.log(`Server is running at http://localhost:${PORT}`)
})

程序员小洛文档