Compare commits

..

No commits in common. "29173ca55ea9a741c27ccc4d31d4c355843f2b48" and "edadb0e082c2311f28a09ee43251d80ce4620c73" have entirely different histories.

4 changed files with 0 additions and 715 deletions

View File

@ -1,334 +0,0 @@
# 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 ✓

View File

@ -1,227 +0,0 @@
# VerticalProgressBar 组件设计
**日期**2026-06-02
**作者**zerosaturation
**状态**:设计中
## 背景与目标
`support-activity` 页面(生日应援活动)目前仅在 [ThemeBanner.vue](../../../frontend/pages/support-activity/components/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从底部至当前圆底部
│ │
│ │
└───┘
```
### 模板
```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 }" />
<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>
```
### 计算公式
```js
// 进度比例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 关键点
```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 = 0``ratio = 0`,当前圆停在底部
## 边界与错误处理
| 场景 | 行为 |
|------|------|
| `target = 0` | `ratio = 0`,圆停在底部,无报错 |
| `current > target` | `ratio``Math.min` 截断为 1圆停在顶部 |
| `current < 0` | 透传UI 表现圆停在 `ratio < 0` 的位置(视觉上圆略微被遮挡) |
| 父组件不传任何 prop | 使用全部默认值current=0, target=100|
## 使用方式
**不修改 `ThemeBanner.vue`**,在 `support-activity/index.vue` 中以兄弟节点方式使用:
```vue
<VerticalProgressBar
:current="progressData.current"
:target="progressData.target"
/>
```
页面已通过 `progressManager` 实时更新 `progressData.current`,因此组件会自动响应。
## 测试
> 单元测试不在本次范围。后续可补:
>
> - `ratio` 计算target=0、current>target、current=负数等)
> - watch 触发 `toLocaleString` 次数
> - 视觉回归(手动)
## 待办与未来扩展
- [ ] 替换为非线性/缓动公式(按用户后续设计)
- [ ] 加入 stage 段位刻度(沿 bar 显示 N 段)
- [ ] 完成后切换为"庆祝态"动画
- [ ] 提取公共 composable`useRatio`)便于其他页面复用

View File

@ -1,146 +0,0 @@
<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>

View File

@ -21,13 +21,6 @@
@tap="openRankingModal"
/>
<!-- 竖向进度条fixed 定位左侧 -->
<VerticalProgressBar
v-if="progressData.target > 0"
:current="progressData.current"
:target="progressData.target"
/>
<!-- 实时贡献列表 -->
<ContributionList
v-if="activityId && !isLoading"
@ -133,7 +126,6 @@ import performanceMonitor from '@/utils/performance-monitor'
import Header from '../components/Header.vue';
import BottomNav from '../components/BottomNav.vue';
import ThemeBanner from './components/ThemeBanner.vue'
import VerticalProgressBar from './components/VerticalProgressBar.vue'
import ContributionList from './components/ContributionList.vue'
import StageArea from './components/StageArea.vue'
import FloatingBubbles from './components/FloatingBubbles.vue'