Skip to content

📊 进度条组件

Bubble 进度条组件提供了丰富的进度显示功能,支持单任务和多任务进度跟踪,适用于各种数据处理和任务执行场景。

🎯 组件概览

功能特性

  • 单任务进度 - 简单的进度条显示
  • 多任务进度 - 并行任务进度管理
  • 实时更新 - 动态进度状态更新
  • 自定义样式 - 灵活的视觉定制
  • 错误处理 - 失败状态显示和重试机制

📝 基础用法

简单进度条

go
// pkg/bubble/progress_simple.go
package bubble

import (
    "fmt"
    "strings"
    "time"
    
    "github.com/charmbracelet/lipgloss"
)

// 简单进度条模型
type SimpleProgressModel struct {
    current int
    total   int
    title   string
    width   int
    style   ProgressStyle
}

// 进度条样式
type ProgressStyle struct {
    Filled   lipgloss.Style
    Empty    lipgloss.Style
    Percent  lipgloss.Style
    Title    lipgloss.Style
}

// 默认样式
func DefaultProgressStyle() ProgressStyle {
    return ProgressStyle{
        Filled: lipgloss.NewStyle().
            Foreground(lipgloss.Color("#00FF00")).
            Background(lipgloss.Color("#004000")),
        Empty: lipgloss.NewStyle().
            Foreground(lipgloss.Color("#404040")).
            Background(lipgloss.Color("#202020")),
        Percent: lipgloss.NewStyle().
            Foreground(lipgloss.Color("#FFFFFF")).
            Bold(true),
        Title: lipgloss.NewStyle().
            Foreground(lipgloss.Color("#00FFFF")).
            Bold(true),
    }
}

// 创建简单进度条
func NewSimpleProgress(title string, total int) SimpleProgressModel {
    return SimpleProgressModel{
        current: 0,
        total:   total,
        title:   title,
        width:   50,
        style:   DefaultProgressStyle(),
    }
}

// 设置当前进度
func (m SimpleProgressModel) SetProgress(current int) SimpleProgressModel {
    if current > m.total {
        current = m.total
    }
    if current < 0 {
        current = 0
    }
    
    m.current = current
    return m
}

// 增加进度
func (m SimpleProgressModel) Increment() SimpleProgressModel {
    return m.SetProgress(m.current + 1)
}

// 设置宽度
func (m SimpleProgressModel) WithWidth(width int) SimpleProgressModel {
    m.width = width
    return m
}

// 设置样式
func (m SimpleProgressModel) WithStyle(style ProgressStyle) SimpleProgressModel {
    m.style = style
    return m
}

// 计算进度百分比
func (m SimpleProgressModel) Percentage() float64 {
    if m.total == 0 {
        return 0
    }
    return float64(m.current) / float64(m.total) * 100
}

// 是否完成
func (m SimpleProgressModel) IsComplete() bool {
    return m.current >= m.total
}

// 渲染视图
func (m SimpleProgressModel) View() string {
    var b strings.Builder
    
    // 标题
    if m.title != "" {
        b.WriteString(m.style.Title.Render(m.title))
        b.WriteString("\n")
    }
    
    // 进度条
    filledWidth := int(float64(m.width) * m.Percentage() / 100)
    emptyWidth := m.width - filledWidth
    
    filled := strings.Repeat("█", filledWidth)
    empty := strings.Repeat("░", emptyWidth)
    
    progressBar := m.style.Filled.Render(filled) + m.style.Empty.Render(empty)
    b.WriteString(progressBar)
    
    // 百分比和计数
    percent := m.style.Percent.Render(fmt.Sprintf(" %.1f%% (%d/%d)", 
        m.Percentage(), m.current, m.total))
    b.WriteString(percent)
    
    return b.String()
}

// 使用示例
func ExampleSimpleProgress() {
    progress := NewSimpleProgress("下载文件", 100)
    
    for i := 0; i <= 100; i++ {
        progress = progress.SetProgress(i)
        fmt.Print("\r" + progress.View())
        time.Sleep(50 * time.Millisecond)
    }
    fmt.Println()
}

多任务进度管理

go
// pkg/bubble/progress_multi.go
package bubble

import (
    "fmt"
    "sort"
    "strings"
    "sync"
    "time"
)

// 任务状态
type TaskStatus int

const (
    StatusPending TaskStatus = iota
    StatusRunning
    StatusCompleted
    StatusFailed
    StatusCancelled
)

func (s TaskStatus) String() string {
    switch s {
    case StatusPending:
        return "待执行"
    case StatusRunning:
        return "执行中"
    case StatusCompleted:
        return "已完成"
    case StatusFailed:
        return "失败"
    case StatusCancelled:
        return "已取消"
    default:
        return "未知"
    }
}

// 任务进度
type TaskProgress struct {
    ID          string
    Name        string
    Status      TaskStatus
    Current     int
    Total       int
    Error       error
    StartTime   time.Time
    EndTime     time.Time
    metadata    map[string]interface{}
    mu          sync.RWMutex
}

// 创建新任务
func NewTaskProgress(id, name string, total int) *TaskProgress {
    return &TaskProgress{
        ID:       id,
        Name:     name,
        Status:   StatusPending,
        Current:  0,
        Total:    total,
        metadata: make(map[string]interface{}),
    }
}

// 启动任务
func (t *TaskProgress) Start() {
    t.mu.Lock()
    defer t.mu.Unlock()
    
    t.Status = StatusRunning
    t.StartTime = time.Now()
}

// 更新进度
func (t *TaskProgress) UpdateProgress(current int) {
    t.mu.Lock()
    defer t.mu.Unlock()
    
    if current > t.Total {
        current = t.Total
    }
    if current < 0 {
        current = 0
    }
    
    t.Current = current
    
    if current >= t.Total && t.Status == StatusRunning {
        t.Status = StatusCompleted
        t.EndTime = time.Now()
    }
}

// 增加进度
func (t *TaskProgress) Increment() {
    t.UpdateProgress(t.Current + 1)
}

// 标记失败
func (t *TaskProgress) Fail(err error) {
    t.mu.Lock()
    defer t.mu.Unlock()
    
    t.Status = StatusFailed
    t.Error = err
    t.EndTime = time.Now()
}

// 取消任务
func (t *TaskProgress) Cancel() {
    t.mu.Lock()
    defer t.mu.Unlock()
    
    t.Status = StatusCancelled
    t.EndTime = time.Now()
}

// 获取进度百分比
func (t *TaskProgress) Percentage() float64 {
    t.mu.RLock()
    defer t.mu.RUnlock()
    
    if t.Total == 0 {
        return 0
    }
    return float64(t.Current) / float64(t.Total) * 100
}

// 获取执行时间
func (t *TaskProgress) Duration() time.Duration {
    t.mu.RLock()
    defer t.mu.RUnlock()
    
    if t.StartTime.IsZero() {
        return 0
    }
    
    endTime := t.EndTime
    if endTime.IsZero() {
        endTime = time.Now()
    }
    
    return endTime.Sub(t.StartTime)
}

// 设置元数据
func (t *TaskProgress) SetMetadata(key string, value interface{}) {
    t.mu.Lock()
    defer t.mu.Unlock()
    
    t.metadata[key] = value
}

// 获取元数据
func (t *TaskProgress) GetMetadata(key string) interface{} {
    t.mu.RLock()
    defer t.mu.RUnlock()
    
    return t.metadata[key]
}

// 多任务进度管理器
type MultiProgressManager struct {
    tasks    map[string]*TaskProgress
    order    []string // 任务显示顺序
    mu       sync.RWMutex
    style    MultiProgressStyle
    maxWidth int
}

// 多任务样式
type MultiProgressStyle struct {
    TaskName    lipgloss.Style
    ProgressBar ProgressStyle
    Status      map[TaskStatus]lipgloss.Style
    Summary     lipgloss.Style
}

// 默认多任务样式
func DefaultMultiProgressStyle() MultiProgressStyle {
    return MultiProgressStyle{
        TaskName: lipgloss.NewStyle().
            Foreground(lipgloss.Color("#FFFFFF")).
            Bold(true),
        ProgressBar: DefaultProgressStyle(),
        Status: map[TaskStatus]lipgloss.Style{
            StatusPending: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#808080")),
            StatusRunning: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#00FF00")),
            StatusCompleted: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#00FF00")),
            StatusFailed: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#FF0000")),
            StatusCancelled: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#FFFF00")),
        },
        Summary: lipgloss.NewStyle().
            Foreground(lipgloss.Color("#00FFFF")).
            Bold(true),
    }
}

// 创建多任务进度管理器
func NewMultiProgressManager() *MultiProgressManager {
    return &MultiProgressManager{
        tasks:    make(map[string]*TaskProgress),
        order:    make([]string, 0),
        style:    DefaultMultiProgressStyle(),
        maxWidth: 80,
    }
}

// 添加任务
func (m *MultiProgressManager) AddTask(task *TaskProgress) {
    m.mu.Lock()
    defer m.mu.Unlock()
    
    m.tasks[task.ID] = task
    m.order = append(m.order, task.ID)
}

// 获取任务
func (m *MultiProgressManager) GetTask(id string) *TaskProgress {
    m.mu.RLock()
    defer m.mu.RUnlock()
    
    return m.tasks[id]
}

// 移除任务
func (m *MultiProgressManager) RemoveTask(id string) {
    m.mu.Lock()
    defer m.mu.Unlock()
    
    delete(m.tasks, id)
    
    for i, taskID := range m.order {
        if taskID == id {
            m.order = append(m.order[:i], m.order[i+1:]...)
            break
        }
    }
}

// 获取所有任务
func (m *MultiProgressManager) GetAllTasks() []*TaskProgress {
    m.mu.RLock()
    defer m.mu.RUnlock()
    
    tasks := make([]*TaskProgress, len(m.order))
    for i, id := range m.order {
        tasks[i] = m.tasks[id]
    }
    
    return tasks
}

// 获取统计信息
func (m *MultiProgressManager) GetStats() (completed, failed, running, pending int) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    
    for _, task := range m.tasks {
        switch task.Status {
        case StatusCompleted:
            completed++
        case StatusFailed:
            failed++
        case StatusRunning:
            running++
        case StatusPending:
            pending++
        }
    }
    
    return
}

// 是否全部完成
func (m *MultiProgressManager) IsAllCompleted() bool {
    completed, failed, running, pending := m.GetStats()
    return running == 0 && pending == 0
}

// 设置样式
func (m *MultiProgressManager) WithStyle(style MultiProgressStyle) *MultiProgressManager {
    m.style = style
    return m
}

// 设置最大宽度
func (m *MultiProgressManager) WithMaxWidth(width int) *MultiProgressManager {
    m.maxWidth = width
    return m
}

// 渲染单个任务
func (m *MultiProgressManager) renderTask(task *TaskProgress) string {
    var b strings.Builder
    
    // 任务名称和状态
    nameWidth := 30
    if len(task.Name) > nameWidth {
        task.Name = task.Name[:nameWidth-3] + "..."
    }
    
    statusStyle := m.style.Status[task.Status]
    taskInfo := fmt.Sprintf("%-*s [%s]", nameWidth, task.Name, task.Status.String())
    b.WriteString(m.style.TaskName.Render(taskInfo))
    b.WriteString(" ")
    
    // 进度条
    progressWidth := m.maxWidth - nameWidth - 20 // 留出空间给状态和百分比
    if progressWidth < 10 {
        progressWidth = 10
    }
    
    filledWidth := int(float64(progressWidth) * task.Percentage() / 100)
    emptyWidth := progressWidth - filledWidth
    
    filled := strings.Repeat("█", filledWidth)
    empty := strings.Repeat("░", emptyWidth)
    
    progressBar := m.style.ProgressBar.Filled.Render(filled) + 
                  m.style.ProgressBar.Empty.Render(empty)
    b.WriteString(progressBar)
    
    // 百分比和时间
    percent := fmt.Sprintf(" %.1f%% ", task.Percentage())
    b.WriteString(m.style.ProgressBar.Percent.Render(percent))
    
    if task.Status == StatusRunning || task.Status == StatusCompleted {
        duration := task.Duration()
        b.WriteString(fmt.Sprintf("(%v)", duration.Round(time.Second)))
    }
    
    // 错误信息
    if task.Error != nil {
        b.WriteString("\n")
        errorMsg := fmt.Sprintf("    错误: %s", task.Error.Error())
        b.WriteString(m.style.Status[StatusFailed].Render(errorMsg))
    }
    
    return b.String()
}

// 渲染视图
func (m *MultiProgressManager) View() string {
    var b strings.Builder
    
    // 任务列表
    tasks := m.GetAllTasks()
    for i, task := range tasks {
        if i > 0 {
            b.WriteString("\n")
        }
        b.WriteString(m.renderTask(task))
    }
    
    // 总体统计
    completed, failed, running, pending := m.GetStats()
    total := len(tasks)
    
    if total > 0 {
        b.WriteString("\n\n")
        summary := fmt.Sprintf("总计: %d | 已完成: %d | 失败: %d | 运行中: %d | 等待: %d",
            total, completed, failed, running, pending)
        b.WriteString(m.style.Summary.Render(summary))
        
        // 总体进度
        if total > 0 {
            overallPercent := float64(completed) / float64(total) * 100
            b.WriteString(fmt.Sprintf(" (%.1f%%)", overallPercent))
        }
    }
    
    return b.String()
}

实时进度更新示例

go
// examples/progress_example.go
package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
    
    "lzt/pkg/bubble"
)

func main() {
    // 创建多任务进度管理器
    manager := bubble.NewMultiProgressManager()
    
    // 添加多个任务
    tasks := []*bubble.TaskProgress{
        bubble.NewTaskProgress("download1", "下载文件 1", 100),
        bubble.NewTaskProgress("download2", "下载文件 2", 150),
        bubble.NewTaskProgress("process1", "处理数据 1", 80),
        bubble.NewTaskProgress("process2", "处理数据 2", 120),
    }
    
    for _, task := range tasks {
        manager.AddTask(task)
    }
    
    // 启动任务处理
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
    for _, task := range tasks {
        go simulateTaskExecution(ctx, task)
    }
    
    // 实时显示进度
    for !manager.IsAllCompleted() {
        // 清屏并显示进度
        fmt.Print("\033[H\033[2J") // ANSI 清屏
        fmt.Println(manager.View())
        
        time.Sleep(100 * time.Millisecond)
    }
    
    // 显示最终结果
    fmt.Print("\033[H\033[2J")
    fmt.Println(manager.View())
    fmt.Println("\n所有任务已完成!")
}

func simulateTaskExecution(ctx context.Context, task *bubble.TaskProgress) {
    // 随机延迟启动
    time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
    
    task.Start()
    
    for i := 0; i <= task.Total; i++ {
        select {
        case <-ctx.Done():
            task.Cancel()
            return
        default:
        }
        
        // 模拟工作
        time.Sleep(time.Duration(rand.Intn(50)+10) * time.Millisecond)
        
        // 随机失败
        if rand.Float64() < 0.01 { // 1% 失败率
            task.Fail(fmt.Errorf("随机失败"))
            return
        }
        
        task.UpdateProgress(i)
    }
}

🎨 自定义样式

主题样式配置

go
// pkg/bubble/themes.go
package bubble

import "github.com/charmbracelet/lipgloss"

// 暗色主题
func DarkTheme() MultiProgressStyle {
    return MultiProgressStyle{
        TaskName: lipgloss.NewStyle().
            Foreground(lipgloss.Color("#E0E0E0")).
            Bold(true),
        ProgressBar: ProgressStyle{
            Filled: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#00FF7F")).
                Background(lipgloss.Color("#003225")),
            Empty: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#404040")).
                Background(lipgloss.Color("#1A1A1A")),
            Percent: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#FFFFFF")).
                Bold(true),
            Title: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#00BFFF")).
                Bold(true),
        },
        Status: map[TaskStatus]lipgloss.Style{
            StatusPending: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#696969")),
            StatusRunning: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#00FF7F")),
            StatusCompleted: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#32CD32")),
            StatusFailed: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#FF4500")),
            StatusCancelled: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#FFD700")),
        },
        Summary: lipgloss.NewStyle().
            Foreground(lipgloss.Color("#00BFFF")).
            Bold(true),
    }
}

// 亮色主题
func LightTheme() MultiProgressStyle {
    return MultiProgressStyle{
        TaskName: lipgloss.NewStyle().
            Foreground(lipgloss.Color("#2F2F2F")).
            Bold(true),
        ProgressBar: ProgressStyle{
            Filled: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#228B22")).
                Background(lipgloss.Color("#E6FFE6")),
            Empty: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#D3D3D3")).
                Background(lipgloss.Color("#F5F5F5")),
            Percent: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#000000")).
                Bold(true),
            Title: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#4682B4")).
                Bold(true),
        },
        Status: map[TaskStatus]lipgloss.Style{
            StatusPending: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#808080")),
            StatusRunning: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#228B22")),
            StatusCompleted: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#006400")),
            StatusFailed: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#DC143C")),
            StatusCancelled: lipgloss.NewStyle().
                Foreground(lipgloss.Color("#FF8C00")),
        },
        Summary: lipgloss.NewStyle().
            Foreground(lipgloss.Color("#4682B4")).
            Bold(true),
    }
}

📚 相关资源

项目文档

外部参考


💡 使用建议: 进度条组件适用于长时间运行的任务。合理设置更新频率,避免过度刷新影响性能。对于大量并发任务,考虑使用分页或折叠显示。

基于 MIT 许可证发布