topfans/docs/superpowers/specs/2026-06-02-vertical-progress-bar-design.md
2026-06-02 21:35:48 +08:00

6.4 KiB
Raw Blame History

VerticalProgressBar 组件设计

日期2026-06-02 作者zerosaturation 状态:设计中

背景与目标

support-activity 页面(生日应援活动)目前仅在 ThemeBanner.vue 右下角以纯文字形式展示进度:

当前进度 19,901,123 / 19,911,005

缺少一个直观的、可视化的进度呈现。本次新增一个独立的竖向进度条组件 VerticalProgressBar,承担"展示当前活动进度"这一职责,并实时反映后端轮询数据。

范围

  • In
    • 新增可复用组件 VerticalProgressBar.vue
    • 包含竖向进度条 + 当前/目标数字
    • 当前进度圆随实时数据沿轨道自下而上移动
  • Out
    • 不修改 ThemeBanner.vue
    • 不在 ThemeBanner.vue 中嵌入此组件
    • 视觉细节(颜色、圆角、阴影等)不调优,由用户后续自行调整

组件设计

文件位置

frontend/pages/support-activity/components/VerticalProgressBar.vue

Props

名称 类型 默认值 说明
current Number 0 当前进度值
target Number 100 目标进度值
barHeight String '200rpx' 轨道总高度
barWidth String '16rpx' 轨道宽度
circleSize String '60rpx' 进度圆直径
fillColor String 'linear-gradient(180deg, #FFD700, #FFA500)' 填充渐变
trackColor String 'rgba(255,255,255,0.2)' 轨道底色
textColor String '#FFD700' 当前数字颜色
targetColor String 'rgba(255,255,255,0.8)' 目标数字颜色
showText Boolean true 是否显示数字(方便单测与单用)

视觉结构

        19,911,005                ← 目标数字(位于目标圆上方)
          ┌─┐
          │ │ ← 目标圆(固定在 bar 顶部)
          ├─┤
          │ │ ← 未填充轨道trackColor
          │ │
          │ │
        ┌─┴─┐
        │   │
        │19,│ ← 当前圆(带数字在内部,随 progress 上移)
        │901│
        │   │
        ├─==┤ ← 已填充fillColor从底部至当前圆底部
        │   │
        │   │
        └───┘

模板

<template>
  <view class="v-progress">
    <text v-if="showText" class="v-target-text">{{ formattedTarget }}</text>
    <view
      class="v-track"
      :style="{ width: barWidth, height: barHeight }"
    >
      <view class="v-fill" :style="{ height: fillHeight }" />
      <view class="v-circle v-target-circle" :style="{ width: circleSize, height: circleSize }" />
      <view
        class="v-circle v-current-circle"
        :style="{ top: circleTop, width: circleSize, height: circleSize }"
      >
        <text v-if="showText" class="v-current-text">{{ formattedCurrent }}</text>
      </view>
    </view>
  </view>
</template>

计算公式

// 进度比例0~1
const ratio = computed(() => {
  if (props.target === 0) return 0
  return Math.min(props.current / props.target, 1)
})

// 填充高度:从底部起,占 bar 的 ratio × 100%
const fillHeight = computed(() => ratio.value * 100 + '%')

// 当前圆位置:从顶部起,(1 - ratio) × 100%
const circleTop = computed(() => (1 - ratio.value) * 100 + '%')

// 数字本地化(避免每帧重建)
const formattedCurrent = ref(props.current.toLocaleString())
const formattedTarget = ref(props.target.toLocaleString())

watch(() => props.current, (v) => { formattedCurrent.value = v.toLocaleString() })
watch(() => props.target, (v) => { formattedTarget.value = v.toLocaleString() })

公式可替换点:未来若要换成非线性/缓动/阈值公式,仅需替换 ratio 这一处计算。

CSS 关键点

.v-progress {
  position: fixed;
  left: 24rpx;
  top: 50%;
  transform: translateY(-50%);
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* 其它样式留空,方便用户自行调 */
}

.v-track {
  position: relative;
  background: v-bind(trackColor);
  border-radius: 999rpx;
  overflow: visible; /* 关键:让圆能露出轨道边界 */
}

.v-fill {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  background: v-bind(fillColor);
  border-radius: 999rpx;
  transition: height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

.v-circle {
  position: absolute;
  left: 50%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.v-target-circle {
  top: 0;
  background: rgba(255, 255, 255, 0.3);
  /* 不放数字 */
}

.v-current-circle {
  background: v-bind(fillColor);
  color: v-bind(textColor);
  transition: top 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

.v-current-text,
.v-target-text {
  font-size: 20rpx;
  line-height: 1;
  white-space: nowrap;
}

.v-target-text {
  color: v-bind(targetColor);
  margin-bottom: 8rpx;
}

行为

  1. 组件挂载后,初始 ratio 决定当前圆位置和填充高度
  2. 父组件更新 current 时,formattedCurrent 通过 watch 重算(避免每帧 toLocaleString
  3. 当前圆 top.v-fill 高度过渡时长 0.4s,使用 ease 曲线
  4. target = 0ratio = 0,当前圆停在底部

边界与错误处理

场景 行为
target = 0 ratio = 0,圆停在底部,无报错
current > target ratioMath.min 截断为 1圆停在顶部
current < 0 透传UI 表现圆停在 ratio < 0 的位置(视觉上圆略微被遮挡)
父组件不传任何 prop 使用全部默认值current=0, target=100

使用方式

不修改 ThemeBanner.vue,在 support-activity/index.vue 中以兄弟节点方式使用:

<VerticalProgressBar
  :current="progressData.current"
  :target="progressData.target"
/>

页面已通过 progressManager 实时更新 progressData.current,因此组件会自动响应。

测试

单元测试不在本次范围。后续可补:

  • ratio 计算target=0、current>target、current=负数等)
  • watch 触发 toLocaleString 次数
  • 视觉回归(手动)

待办与未来扩展

  • 替换为非线性/缓动公式(按用户后续设计)
  • 加入 stage 段位刻度(沿 bar 显示 N 段)
  • 完成后切换为"庆祝态"动画
  • 提取公共 composableuseRatio)便于其他页面复用