Skip to content

Progress 进度条

概述

Progress 进度条组件用于展示操作进度,告知用户当前处理进度,并预估剩余时间。Element Plus 的 Progress 组件支持多种样式、动画效果和自定义配置,适用于文件上传、数据加载、任务处理等场景。

学习目标

  • 掌握 Progress 组件的基础用法
  • 理解不同类型和样式的进度条
  • 学会使用环形进度条和仪表盘进度条
  • 掌握进度条的动画和自定义配置
  • 了解进度条在实际项目中的应用

基础用法

线形进度条

最基础的进度条,通过 percentage 属性设置进度:

vue
<template>
  <div class="progress-demo">
    <el-progress :percentage="50" />
    <el-progress :percentage="100" :format="format" />
    <el-progress :percentage="100" status="success" />
    <el-progress :percentage="100" status="warning" />
    <el-progress :percentage="50" status="exception" />
  </div>
</template>

<script setup>
const format = (percentage) => {
  return percentage === 100 ? '满' : `${percentage}%`
}
</script>

<style>
.progress-demo .el-progress {
  margin-bottom: 15px;
}
</style>

百分比内显

通过 text-inside 属性可以将进度条描述置于进度条内部:

vue
<template>
  <div class="progress-demo">
    <el-progress :percentage="70" :text-inside="true" :stroke-width="26" />
    <el-progress :percentage="100" :text-inside="true" :stroke-width="24" status="success" />
    <el-progress :percentage="80" :text-inside="true" :stroke-width="22" status="warning" />
    <el-progress :percentage="50" :text-inside="true" :stroke-width="20" status="exception" />
  </div>
</template>

自定义颜色

通过 color 属性设置进度条的颜色,支持字符串、函数和数组:

vue
<template>
  <div class="progress-demo">
    <!-- 字符串颜色 -->
    <el-progress :percentage="percentage" color="#8e71c7" />
    
    <!-- 函数颜色 -->
    <el-progress :percentage="percentage" :color="customColorMethod" />
    
    <!-- 数组颜色(渐变) -->
    <el-progress :percentage="percentage" :color="customColors" />
    
    <div class="demo-progress-controls">
      <el-button-group>
        <el-button icon="Minus" @click="decrease" />
        <el-button icon="Plus" @click="increase" />
      </el-button-group>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Minus, Plus } from '@element-plus/icons-vue'

const percentage = ref(20)

const customColorMethod = (percentage) => {
  if (percentage < 30) {
    return '#909399'
  } else if (percentage < 70) {
    return '#e6a23c'
  } else {
    return '#67c23a'
  }
}

const customColors = [
  { color: '#f56c6c', percentage: 20 },
  { color: '#e6a23c', percentage: 40 },
  { color: '#5cb87a', percentage: 60 },
  { color: '#1989fa', percentage: 80 },
  { color: '#6f7ad3', percentage: 100 }
]

const increase = () => {
  percentage.value += 10
  if (percentage.value > 100) {
    percentage.value = 100
  }
}

const decrease = () => {
  percentage.value -= 10
  if (percentage.value < 0) {
    percentage.value = 0
  }
}
</script>

环形进度条

基础环形进度条

通过 type="circle" 设置环形进度条:

vue
<template>
  <div class="circle-progress-demo">
    <el-progress type="circle" :percentage="0" />
    <el-progress type="circle" :percentage="25" />
    <el-progress type="circle" :percentage="100" status="success" />
    <el-progress type="circle" :percentage="70" status="warning" />
    <el-progress type="circle" :percentage="50" status="exception" />
  </div>
</template>

<style>
.circle-progress-demo {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}
</style>

自定义尺寸的环形进度条

通过 width 属性设置环形进度条的尺寸:

vue
<template>
  <div class="circle-progress-demo">
    <el-progress type="circle" :percentage="percentage" :width="80" />
    <el-progress type="circle" :percentage="percentage" :width="120" />
    <el-progress type="circle" :percentage="percentage" :width="140" />
    
    <div class="demo-progress-controls">
      <el-button-group>
        <el-button icon="Minus" @click="decrease" />
        <el-button icon="Plus" @click="increase" />
      </el-button-group>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Minus, Plus } from '@element-plus/icons-vue'

const percentage = ref(50)

const increase = () => {
  percentage.value += 10
  if (percentage.value > 100) {
    percentage.value = 100
  }
}

const decrease = () => {
  percentage.value -= 10
  if (percentage.value < 0) {
    percentage.value = 0
  }
}
</script>

仪表盘进度条

仪表盘样式

通过 type="dashboard" 设置仪表盘进度条:

vue
<template>
  <div class="dashboard-progress-demo">
    <el-progress type="dashboard" :percentage="percentage" :color="colors" />
    
    <div class="demo-progress-controls">
      <el-button-group>
        <el-button icon="Minus" @click="decrease" />
        <el-button icon="Plus" @click="increase" />
      </el-button-group>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Minus, Plus } from '@element-plus/icons-vue'

const percentage = ref(80)

const colors = [
  { color: '#f56c6c', percentage: 20 },
  { color: '#e6a23c', percentage: 40 },
  { color: '#5cb87a', percentage: 60 },
  { color: '#1989fa', percentage: 80 },
  { color: '#6f7ad3', percentage: 100 }
]

const increase = () => {
  percentage.value += 10
  if (percentage.value > 100) {
    percentage.value = 100
  }
}

const decrease = () => {
  percentage.value -= 10
  if (percentage.value < 0) {
    percentage.value = 0
  }
}
</script>

动画进度条

动态进度条

结合定时器实现动态进度条效果:

vue
<template>
  <div class="animated-progress-demo">
    <h4>自动进度条</h4>
    <el-progress :percentage="autoPercentage" :color="autoColors" />
    
    <h4>加载进度条</h4>
    <el-progress 
      :percentage="loadingPercentage" 
      :text-inside="true" 
      :stroke-width="20"
      status="success"
    />
    
    <h4>环形动画进度条</h4>
    <el-progress 
      type="circle" 
      :percentage="circlePercentage" 
      :color="circleColors"
      :width="120"
    />
    
    <div class="demo-progress-controls">
      <el-button @click="startAnimation">开始动画</el-button>
      <el-button @click="stopAnimation">停止动画</el-button>
      <el-button @click="resetAnimation">重置</el-button>
    </div>
  </div>
</template>

<script setup>
import { ref, onUnmounted } from 'vue'

const autoPercentage = ref(0)
const loadingPercentage = ref(0)
const circlePercentage = ref(0)

let timer = null

const autoColors = [
  { color: '#f56c6c', percentage: 20 },
  { color: '#e6a23c', percentage: 40 },
  { color: '#5cb87a', percentage: 60 },
  { color: '#1989fa', percentage: 80 },
  { color: '#6f7ad3', percentage: 100 }
]

const circleColors = [
  { color: '#909399', percentage: 20 },
  { color: '#e6a23c', percentage: 40 },
  { color: '#67c23a', percentage: 60 },
  { color: '#409eff', percentage: 80 },
  { color: '#67c23a', percentage: 100 }
]

const startAnimation = () => {
  if (timer) clearInterval(timer)
  
  timer = setInterval(() => {
    autoPercentage.value += 1
    loadingPercentage.value += 2
    circlePercentage.value += 1.5
    
    if (autoPercentage.value >= 100) {
      autoPercentage.value = 100
    }
    if (loadingPercentage.value >= 100) {
      loadingPercentage.value = 100
    }
    if (circlePercentage.value >= 100) {
      circlePercentage.value = 100
      clearInterval(timer)
      timer = null
    }
  }, 50)
}

const stopAnimation = () => {
  if (timer) {
    clearInterval(timer)
    timer = null
  }
}

const resetAnimation = () => {
  stopAnimation()
  autoPercentage.value = 0
  loadingPercentage.value = 0
  circlePercentage.value = 0
}

onUnmounted(() => {
  if (timer) {
    clearInterval(timer)
  }
})
</script>

<style>
.animated-progress-demo .el-progress {
  margin-bottom: 15px;
}

.animated-progress-demo .demo-progress-controls {
  margin-top: 20px;
}

.animated-progress-demo .demo-progress-controls .el-button {
  margin-right: 10px;
}
</style>

实际应用示例

文件上传进度管理器

一个完整的文件上传进度管理示例:

vue
<template>
  <div class="file-upload-manager">
    <div class="upload-area">
      <el-upload
        ref="uploadRef"
        class="upload-demo"
        drag
        action="#"
        multiple
        :auto-upload="false"
        :on-change="handleFileChange"
        :file-list="fileList"
      >
        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
        <div class="el-upload__text">
          将文件拖到此处,或<em>点击上传</em>
        </div>
        <template #tip>
          <div class="el-upload__tip">
            支持 jpg/png/gif/pdf 文件,且不超过 500kb
          </div>
        </template>
      </el-upload>
      
      <div class="upload-actions">
        <el-button type="primary" @click="startUpload" :disabled="!fileList.length || uploading">
          开始上传
        </el-button>
        <el-button @click="clearFiles" :disabled="uploading">
          清空文件
        </el-button>
        <el-button @click="pauseUpload" v-if="uploading">
          暂停上传
        </el-button>
      </div>
    </div>
    
    <!-- 上传进度列表 -->
    <div class="upload-progress-list" v-if="fileList.length">
      <h4>上传进度</h4>
      
      <!-- 总体进度 -->
      <div class="overall-progress">
        <h5>总体进度</h5>
        <el-progress 
          :percentage="overallProgress" 
          :color="overallProgressColor"
          :text-inside="true"
          :stroke-width="20"
        />
        <div class="progress-info">
          <span>已完成: {{ completedFiles }}/{{ fileList.length }}</span>
          <span>总大小: {{ formatFileSize(totalSize) }}</span>
          <span>已上传: {{ formatFileSize(uploadedSize) }}</span>
        </div>
      </div>
      
      <!-- 单个文件进度 -->
      <div class="file-progress-list">
        <div 
          v-for="file in fileList" 
          :key="file.uid" 
          class="file-progress-item"
        >
          <div class="file-info">
            <div class="file-name">
              <el-icon><Document /></el-icon>
              {{ file.name }}
            </div>
            <div class="file-size">{{ formatFileSize(file.size) }}</div>
            <div class="file-status">
              <el-tag 
                :type="getFileStatusType(file.status)"
                size="small"
              >
                {{ getFileStatusText(file.status) }}
              </el-tag>
            </div>
          </div>
          
          <div class="file-progress">
            <el-progress 
              :percentage="file.progress || 0"
              :color="getFileProgressColor(file.status)"
              :status="file.status === 'error' ? 'exception' : 
                       file.status === 'success' ? 'success' : null"
            />
          </div>
          
          <div class="file-actions">
            <el-button 
              v-if="file.status === 'ready' || file.status === 'uploading'"
              size="small" 
              @click="cancelFile(file)"
            >
              取消
            </el-button>
            <el-button 
              v-if="file.status === 'error'"
              size="small" 
              type="primary"
              @click="retryFile(file)"
            >
              重试
            </el-button>
            <el-button 
              v-if="file.status === 'success'"
              size="small" 
              type="success"
              @click="previewFile(file)"
            >
              预览
            </el-button>
          </div>
        </div>
      </div>
    </div>
    
    <!-- 上传统计 -->
    <div class="upload-statistics" v-if="fileList.length">
      <h4>上传统计</h4>
      <div class="stats-grid">
        <div class="stat-card">
          <div class="stat-value">{{ fileList.length }}</div>
          <div class="stat-label">总文件数</div>
          <el-progress type="circle" :percentage="100" :width="60" color="#409eff" />
        </div>
        <div class="stat-card">
          <div class="stat-value">{{ completedFiles }}</div>
          <div class="stat-label">已完成</div>
          <el-progress 
            type="circle" 
            :percentage="completedFiles / fileList.length * 100" 
            :width="60" 
            color="#67c23a" 
          />
        </div>
        <div class="stat-card">
          <div class="stat-value">{{ errorFiles }}</div>
          <div class="stat-label">失败文件</div>
          <el-progress 
            type="circle" 
            :percentage="errorFiles / fileList.length * 100" 
            :width="60" 
            color="#f56c6c" 
          />
        </div>
        <div class="stat-card">
          <div class="stat-value">{{ uploadSpeed }}</div>
          <div class="stat-label">上传速度</div>
          <el-progress 
            type="dashboard" 
            :percentage="Math.min(uploadSpeed / 10, 100)" 
            :width="80" 
            :color="speedColors"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { UploadFilled, Document } from '@element-plus/icons-vue'

const uploadRef = ref()
const fileList = ref([])
const uploading = ref(false)
const uploadSpeed = ref(0)

let uploadTimer = null
let speedTimer = null
let lastUploadedSize = 0

const totalSize = computed(() => {
  return fileList.value.reduce((total, file) => total + file.size, 0)
})

const uploadedSize = computed(() => {
  return fileList.value.reduce((total, file) => {
    return total + (file.size * (file.progress || 0) / 100)
  }, 0)
})

const overallProgress = computed(() => {
  if (!fileList.value.length) return 0
  return Math.round(uploadedSize.value / totalSize.value * 100)
})

const completedFiles = computed(() => {
  return fileList.value.filter(file => file.status === 'success').length
})

const errorFiles = computed(() => {
  return fileList.value.filter(file => file.status === 'error').length
})

const overallProgressColor = computed(() => {
  const progress = overallProgress.value
  if (progress < 30) return '#f56c6c'
  if (progress < 70) return '#e6a23c'
  return '#67c23a'
})

const speedColors = [
  { color: '#909399', percentage: 20 },
  { color: '#e6a23c', percentage: 40 },
  { color: '#67c23a', percentage: 60 },
  { color: '#409eff', percentage: 80 },
  { color: '#67c23a', percentage: 100 }
]

const handleFileChange = (file, files) => {
  file.status = 'ready'
  file.progress = 0
  fileList.value = files
}

const startUpload = () => {
  uploading.value = true
  lastUploadedSize = 0
  
  // 开始计算上传速度
  speedTimer = setInterval(() => {
    const currentUploadedSize = uploadedSize.value
    uploadSpeed.value = Math.round((currentUploadedSize - lastUploadedSize) / 1024) // KB/s
    lastUploadedSize = currentUploadedSize
  }, 1000)
  
  // 模拟上传过程
  fileList.value.forEach((file, index) => {
    if (file.status === 'ready') {
      uploadFile(file, index)
    }
  })
}

const uploadFile = (file, index) => {
  file.status = 'uploading'
  
  const uploadInterval = setInterval(() => {
    if (!uploading.value) {
      clearInterval(uploadInterval)
      return
    }
    
    file.progress += Math.random() * 10
    
    if (file.progress >= 100) {
      file.progress = 100
      file.status = Math.random() > 0.1 ? 'success' : 'error' // 90% 成功率
      clearInterval(uploadInterval)
      
      // 检查是否所有文件都已完成
      const allCompleted = fileList.value.every(f => 
        f.status === 'success' || f.status === 'error'
      )
      
      if (allCompleted) {
        uploading.value = false
        if (speedTimer) {
          clearInterval(speedTimer)
          speedTimer = null
        }
        ElMessage.success('所有文件上传完成')
      }
    }
  }, 100 + Math.random() * 200) // 随机上传速度
}

const pauseUpload = () => {
  uploading.value = false
  if (speedTimer) {
    clearInterval(speedTimer)
    speedTimer = null
  }
  ElMessage.info('上传已暂停')
}

const clearFiles = () => {
  fileList.value = []
  uploadRef.value.clearFiles()
}

const cancelFile = (file) => {
  const index = fileList.value.findIndex(f => f.uid === file.uid)
  if (index > -1) {
    fileList.value.splice(index, 1)
  }
}

const retryFile = (file) => {
  file.status = 'ready'
  file.progress = 0
  uploadFile(file)
}

const previewFile = (file) => {
  ElMessage.info(`预览文件: ${file.name}`)
}

const formatFileSize = (size) => {
  if (size < 1024) return size + ' B'
  if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB'
  return (size / (1024 * 1024)).toFixed(1) + ' MB'
}

const getFileStatusType = (status) => {
  switch (status) {
    case 'ready': return 'info'
    case 'uploading': return 'primary'
    case 'success': return 'success'
    case 'error': return 'danger'
    default: return 'info'
  }
}

const getFileStatusText = (status) => {
  switch (status) {
    case 'ready': return '等待上传'
    case 'uploading': return '上传中'
    case 'success': return '上传成功'
    case 'error': return '上传失败'
    default: return '未知状态'
  }
}

const getFileProgressColor = (status) => {
  switch (status) {
    case 'uploading': return '#409eff'
    case 'success': return '#67c23a'
    case 'error': return '#f56c6c'
    default: return '#909399'
  }
}

onUnmounted(() => {
  if (uploadTimer) clearInterval(uploadTimer)
  if (speedTimer) clearInterval(speedTimer)
})
</script>

<style scoped>
.file-upload-manager {
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
}

.upload-area {
  margin-bottom: 30px;
}

.upload-actions {
  margin-top: 15px;
  text-align: center;
}

.upload-actions .el-button {
  margin: 0 5px;
}

.upload-progress-list {
  margin-bottom: 30px;
}

.overall-progress {
  margin-bottom: 20px;
  padding: 15px;
  background-color: #f5f7fa;
  border-radius: 8px;
}

.progress-info {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
  font-size: 14px;
  color: #606266;
}

.file-progress-item {
  display: flex;
  align-items: center;
  padding: 15px;
  margin-bottom: 10px;
  background-color: #fafafa;
  border-radius: 8px;
  border: 1px solid #ebeef5;
}

.file-info {
  flex: 0 0 300px;
  margin-right: 15px;
}

.file-name {
  display: flex;
  align-items: center;
  font-weight: 500;
  margin-bottom: 5px;
}

.file-name .el-icon {
  margin-right: 5px;
}

.file-size {
  font-size: 12px;
  color: #909399;
  margin-bottom: 5px;
}

.file-progress {
  flex: 1;
  margin-right: 15px;
}

.file-actions {
  flex: 0 0 auto;
}

.upload-statistics {
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-top: 15px;
}

.stat-card {
  text-align: center;
  padding: 20px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.stat-value {
  font-size: 24px;
  font-weight: bold;
  color: #303133;
  margin-bottom: 5px;
}

.stat-label {
  font-size: 14px;
  color: #606266;
  margin-bottom: 15px;
}
</style>

Element Plus Study Guide