topfans/docs/superpowers/plans/2026-06-02-vertical-progress-bar.md
2026-06-02 21:40:28 +08:00

12 KiB
Raw Blame History

VerticalProgressBar Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a reusable VerticalProgressBar Vue component to the support-activity page that visually displays real-time progress (current / target) with a vertical bar, a target circle at the top, and a current circle that moves up as progress increases.

Architecture: Single self-contained Vue 3 SFC using <script setup>. Position is fixed on the left side of the screen. The component reads current and target props, computes a ratio (0~1), and uses CSS variables to drive the fill height and the current circle's top position. No external state, no bus, no animation library. Used as a sibling of ThemeBanner in support-activity/index.vue (not embedded inside it).

Tech Stack: Vue 3 (<script setup>), uni-app (vue-cli/H5 + 小程序双端), CSS3 transitions, v-bind() in <style> blocks (Vue 3 SFC feature).

Spec: docs/superpowers/specs/2026-06-02-vertical-progress-bar-design.md


File Structure

File Status Responsibility
frontend/pages/support-activity/components/VerticalProgressBar.vue Create The component itself: template, props, computed (ratio, fillHeight, circleTop, formatted numbers), styles
frontend/pages/support-activity/index.vue Modify Import the component and render it as a sibling of ThemeBanner, passing progressData.current and progressData.target

No other files are touched. The plan is intentionally narrow — single component, single integration point, single page. The ThemeBanner.vue is left untouched per the spec's "Out of Scope" section.


Task 1: Create the VerticalProgressBar.vue component

Files:

  • Create: frontend/pages/support-activity/components/VerticalProgressBar.vue

  • Step 1: Write the component SFC with template, script, and scoped styles

Create the file frontend/pages/support-activity/components/VerticalProgressBar.vue with the following exact content:

<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 }"
      />

      <!-- 当前圆(随 progress 上移) -->
      <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>

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

const props = defineProps({
  current: { type: Number, default: 0 },
  target: { type: Number, default: 100 },
  barHeight: { type: String, default: '200rpx' },
  barWidth: { type: String, default: '16rpx' },
  circleSize: { type: String, default: '60rpx' },
  fillColor: {
    type: String,
    default: 'linear-gradient(180deg, #FFD700, #FFA500)'
  },
  trackColor: {
    type: String,
    default: 'rgba(255, 255, 255, 0.2)'
  },
  textColor: { type: String, default: '#FFD700' },
  targetColor: { type: String, default: 'rgba(255, 255, 255, 0.8)' },
  showText: { type: Boolean, default: true }
})

// 进度比例0~1将来要换公式只改这一处
const ratio = computed(() => {
  if (props.target === 0) return 0
  return Math.min(props.current / props.target, 1)
})

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

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

// 数字本地化:用 ref + watch 避免每帧调用 toLocaleString
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()
})
</script>

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

.v-target-text {
  color: v-bind(targetColor);
  font-size: 20rpx;
  line-height: 1;
  margin-bottom: 8rpx;
  white-space: nowrap;
}

.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;
  box-sizing: border-box;
}

.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 {
  font-size: 20rpx;
  line-height: 1;
  white-space: nowrap;
  color: v-bind(textColor);
}
</style>
  • Step 2: Verify the file is syntactically valid by re-reading it

Run: cat /Users/liulujian/Documents/code/TopFansByGithub/frontend/pages/support-activity/components/VerticalProgressBar.vue | head -5 Expected: First line is <template>, file is non-empty.

  • Step 3: Commit
git add frontend/pages/support-activity/components/VerticalProgressBar.vue
git commit -m "feat(support-activity): add VerticalProgressBar component"

Task 2: Wire the component into support-activity/index.vue

Files:

  • Modify: frontend/pages/support-activity/index.vue (import line + render the component as a sibling of ThemeBanner)

  • Step 1: Add the import statement

In frontend/pages/support-activity/index.vue, locate the import block near the bottom of <script setup> (the line import ThemeBanner from './components/ThemeBanner.vue'). Add the following import line immediately after the ThemeBanner import:

import VerticalProgressBar from './components/VerticalProgressBar.vue'

After the edit, the imports block reads (in order, lines ~126-135):

import Header from '../components/Header.vue';
import BottomNav from '../components/BottomNav.vue';
import ThemeBanner from './components/ThemeBanner.vue'
import VerticalProgressBar from './components/VerticalProgressBar.vue'   // <-- new
import ContributionList from './components/ContributionList.vue'
import StageArea from './components/StageArea.vue'
import FloatingBubbles from './components/FloatingBubbles.vue'
import ActivityRankingModal from './components/ActivityRankingModal.vue'
import ActionBar from './components/ActionBar.vue'
  • Step 2: Add the component render block

In the <template> of the same file, locate the <ThemeBanner ... /> block. Immediately after the closing </ThemeBanner> tag (i.e., between </ThemeBanner> and the next <!-- 实时贡献列表 --> comment), insert the following:

    <!-- 竖向进度条fixed 定位左侧 -->
    <VerticalProgressBar
      v-if="progressData.target > 0"
      :current="progressData.current"
      :target="progressData.target"
    />

The inserted block goes inside <view class="activity-container"> but as a sibling of ThemeBanner, not inside it. The exact position after the edit:

    <ThemeBanner
      v-if="config"
      :title="config.title"
      :banner-image="config.bannerImage"
      :current="progressData.current"
      :target="progressData.target"
      :is-stale-data="isStaleData"
      @tap="openRankingModal"
    />

    <!-- 竖向进度条fixed 定位左侧 -->
    <VerticalProgressBar
      v-if="progressData.target > 0"
      :current="progressData.current"
      :target="progressData.target"
    />

    <!-- 实时贡献列表 -->
    <ContributionList
      v-if="activityId && !isLoading"
      :activity-id="activityId"
      class="contribution-list-wrapper"
    />
  • Step 3: Re-read the file head to confirm changes are coherent

Run: head -50 /Users/liulujian/Documents/code/TopFansByGithub/frontend/pages/support-activity/index.vue Expected: The <VerticalProgressBar> render block is visible between </ThemeBanner> and the <ContributionList> comment.

  • Step 4: Commit
git add frontend/pages/support-activity/index.vue
git commit -m "feat(support-activity): mount VerticalProgressBar on activity page"

Task 3: Verify build / lint pass

Files: none modified

  • Step 1: Check that the project lints clean for the new files

Run: cd /Users/liulujian/Documents/code/TopFansByGithub/frontend && npx eslint pages/support-activity/components/VerticalProgressBar.vue pages/support-activity/index.vue 2>&1 | tail -30

Expected (one of):

  • ✓ No problems / no output / no errors
  • Only style warnings already present in the file before changes

If there are real errors (e.g., Parsing error: ... or import not found), fix the actual error and re-run.

  • Step 2: Smoke-check that the Vue files parse by importing them through the build tooling

If the project has a dev script that uses Vite, run: cd /Users/liulujian/Documents/code/TopFansByGithub/frontend && timeout 30 npm run dev:h5 2>&1 | head -40 Expected: Server starts, no compile errors referencing VerticalProgressBar or support-activity/index.vue. If compile errors appear, fix them and retry.

If dev:h5 is not available, skip this step and rely on step 1's lint result.

  • Step 3: Commit any build / lint fixes (if any)

If you had to fix anything in Steps 1 or 2:

git add frontend/pages/support-activity/components/VerticalProgressBar.vue frontend/pages/support-activity/index.vue
git commit -m "fix(support-activity): resolve lint/build issues for VerticalProgressBar"

If nothing was changed, skip this commit.


Self-Review

Spec coverage:

  • Spec §"Props" → Task 1 Step 1 (all 10 props with default values)
  • Spec §"模板" → Task 1 Step 1 (template structure with v-if="showText")
  • Spec §"计算公式" → Task 1 Step 1 (ratio / fillHeight / circleTop with explicit (current/target) formula and replaceability note)
  • Spec §"CSS 关键点" → Task 1 Step 1 (full CSS block with position: fixed, overflow: visible, transitions)
  • Spec §"使用方式" (don't modify ThemeBanner, use as sibling) → Task 2 Steps 1-2
  • Spec §"边界与错误处理" (target=0 guard) → Task 1 Step 1 (early return in ratio computed)
  • Spec §"Out of Scope" (no ThemeBanner changes, no tests) → No task modifies ThemeBanner; no test task created

Placeholder scan:

  • No TBD / TODO / "implement later" markers in the plan
  • All code blocks are complete and runnable
  • No "similar to Task N" references — each task's code is self-contained
  • All file paths are exact

Type consistency:

  • Props defined in Task 1 (current, target, etc.) are read in Task 1's script and passed by name in Task 2's render block ✓
  • progressData.current / progressData.target are read in index.vue script (pre-existing) and bound to :current / :target in Task 2 ✓
  • ratio / fillHeight / circleTop are defined and used only in Task 1 ✓