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

335 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```vue
<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**
```bash
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:
```js
import VerticalProgressBar from './components/VerticalProgressBar.vue'
```
After the edit, the imports block reads (in order, lines ~126-135):
```js
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:
```vue
<!-- 竖向进度条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:
```vue
<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**
```bash
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:
```bash
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 ✓