# bindbox-mini 组件化重构设计
## 架构目标
将三个活动页面(yifanshang/duiduipeng/wuxianshang)共约5079行代码减少至约2500行,消除61%的冗余。
---
## 架构设计图
```mermaid
graph TB
subgraph Utils[工具层 utils/]
A1[activity.js
活动相关工具函数]
A2[format.js
格式化工具]
A3[cache.js
缓存管理]
end
subgraph Composables[组合式函数 composables/]
B1[useActivity.js
活动数据管理]
B2[useIssues.js
期数据管理]
B3[useRewards.js
奖励数据管理]
B4[usePayment.js
支付流程]
end
subgraph Components[组件层 components/]
subgraph Layout[布局组件]
C1[ActivityPageLayout.vue
活动页面框架]
C2[ActivityHeader.vue
头部卡片]
end
subgraph Biz[业务组件]
C3[ActivityTabs.vue
Tab切换]
C4[RewardsPopup.vue
奖品弹窗]
C5[RecordsList.vue
购买记录]
C6[RewardsPreview.vue
奖池预览]
end
subgraph Existing[已有组件]
C7[PaymentPopup.vue]
C8[FlipGrid.vue]
end
end
subgraph Pages[页面层 pages/activity/]
D1[yifanshang - 选号+专属业务]
D2[duiduipeng - 对对碰游戏+专属业务]
D3[wuxianshang - 多档抽奖+专属业务]
end
Utils --> Composables
Composables --> Pages
Components --> Pages
```
---
## 详细模块设计
### 1. 工具函数层 `utils/`
#### `utils/activity.js` - 活动相关工具 [NEW]
```javascript
// 数据标准化
export function unwrap(list) { /* ... */ }
export function normalizeIssues(list) { /* ... */ }
export function normalizeRewards(list) { /* ... */ }
// 值判断
export function truthy(v) { /* ... */ }
export function detectBoss(i) { /* ... */ }
export function levelToAlpha(level) { /* ... */ }
// 状态转换
export function statusToText(s) { /* ... */ }
```
#### `utils/format.js` - 格式化工具 [NEW]
```javascript
export function cleanUrl(u) { /* ... */ }
export function formatPercent(v) { /* ... */ }
export function formatDateTime(v) { /* ... */ }
export function formatPrice(cents) { /* ... */ }
```
#### `utils/cache.js` - 缓存管理 [NEW]
```javascript
export function isFresh(ts, ttl = 24 * 60 * 60 * 1000) { /* ... */ }
export function getRewardCache() { /* ... */ }
export function setRewardCache(activityId, issueId, value) { /* ... */ }
```
---
### 2. 组合式函数层 `composables/`
#### `composables/useActivity.js` [NEW]
```javascript
export function useActivity(activityId) {
const detail = ref({})
const coverUrl = computed(() => cleanUrl(detail.value?.image || detail.value?.banner || ''))
const statusText = computed(() => statusToText(detail.value?.status))
const pricePerDraw = computed(() => (Number(detail.value?.price_draw || 0) / 100))
async function fetchDetail() { /* ... */ }
return { detail, coverUrl, statusText, pricePerDraw, fetchDetail }
}
```
#### `composables/useIssues.js` [NEW]
```javascript
export function useIssues(activityId) {
const issues = ref([])
const selectedIssueIndex = ref(0)
const currentIssueId = computed(() => issues.value[selectedIssueIndex.value]?.id || '')
const currentIssueTitle = computed(() => /* ... */)
async function fetchIssues() { /* ... */ }
function prevIssue() { /* ... */ }
function nextIssue() { /* ... */ }
function setSelectedById(id) { /* ... */ }
return { issues, selectedIssueIndex, currentIssueId, currentIssueTitle, fetchIssues, prevIssue, nextIssue, setSelectedById }
}
```
#### `composables/useRewards.js` [NEW]
```javascript
export function useRewards(activityId, currentIssueId) {
const rewardsMap = ref({})
const currentIssueRewards = computed(() => rewardsMap.value[currentIssueId.value] || [])
const rewardGroups = computed(() => /* 按level分组 */)
async function fetchRewardsForIssues(issueList) { /* 带缓存 */ }
return { rewardsMap, currentIssueRewards, rewardGroups, fetchRewardsForIssues }
}
```
#### `composables/useRecords.js` [NEW]
```javascript
export function useRecords() {
const winRecords = ref([])
async function fetchWinRecords(activityId, issueId) { /* ... */ }
return { winRecords, fetchWinRecords }
}
```
---
### 3. 组件层 `components/`
#### `ActivityPageLayout.vue` [NEW] - 页面框架组件
Props:
- `coverUrl: String` - 背景图URL
Slots:
- `header` - 头部卡片区域
- `content` - 主要内容(tabs等)
- `footer` - 底部操作栏
- `modals` - 弹窗区域
#### `ActivityHeader.vue` [NEW] - 头部卡片
Props:
- `title: String`
- `price: Number` (分)
- `priceUnit: String` - 价格单位(如"/发"、"/次")
- `coverUrl: String`
- `tags: Array`
- `scheduledTime: String` (可选)
Events:
- `@show-rules`
- `@go-cabinet`
#### `ActivityTabs.vue` [NEW] - Tab切换
Props:
- `modelValue: String` - 当前tab ('pool' | 'records')
- `tabs: Array<{key, label}>`
Events:
- `@update:modelValue`
#### `RewardsPreview.vue` [NEW] - 奖池预览
Props:
- `rewards: Array`
- `grouped: Boolean` - 是否按等级分组显示
#### `RewardsPopup.vue` [NEW] - 奖品弹窗
Props:
- `visible: Boolean`
- `title: String`
- `rewardGroups: Array` - 按等级分组的奖励
Events:
- `@update:visible`
#### `RecordsList.vue` [NEW] - 购买记录列表
Props:
- `records: Array`
- `emptyText: String`
---
### 4. 样式层 `styles/`
#### `styles/activity-common.scss` [NEW]
提取共用样式(约600行):
- 页面布局:`.page-wrapper`, `.bg-decoration`, `.orb`, `@keyframes float`
- 背景处理:`.page-bg`, `.bg-image`, `.bg-mask`
- 入场动画:`.animate-enter`, `.stagger-*`
- 头部卡片样式(可在ActivityHeader组件内联)
- 板块容器:`.section-container`, `.section-header`
- Tabs样式(可在ActivityTabs组件内联)
- 预览列表:`.preview-scroll`, `.preview-item`
- 记录列表:`.records-list`, `.record-item`
- 弹窗样式(可在RewardsPopup组件内联)
---
## 重构后页面结构示例
### yifanshang/index.vue (预计约400行→优化后)
```vue
```
---
## 文件变更清单
### 新增文件
| 文件路径 | 行数估算 | 说明 |
|----------|---------|------|
| `utils/activity.js` | ~80 | 活动工具函数 |
| `utils/format.js` | ~50 | 格式化工具 |
| `utils/cache.js` | ~40 | 缓存管理 |
| `composables/useActivity.js` | ~50 | 活动数据composable |
| `composables/useIssues.js` | ~80 | 期数据composable |
| `composables/useRewards.js` | ~80 | 奖励数据composable |
| `composables/useRecords.js` | ~40 | 记录composable |
| `components/ActivityPageLayout.vue` | ~150 | 页面框架 |
| `components/ActivityHeader.vue` | ~200 | 头部卡片 |
| `components/ActivityTabs.vue` | ~100 | Tab切换 |
| `components/RewardsPreview.vue` | ~120 | 奖池预览 |
| `components/RewardsPopup.vue` | ~150 | 奖品弹窗 |
| `components/RecordsList.vue` | ~80 | 记录列表 |
| **小计** | **~1220** | |
### 修改文件
| 文件路径 | 原行数 | 预计行数 | 变化 |
|----------|-------|---------|------|
| `yifanshang/index.vue` | 1229 | ~400 | -829 |
| `duiduipeng/index.vue` | 2291 | ~800 | -1491 |
| `wuxianshang/index.vue` | 1559 | ~500 | -1059 |
| **小计** | **5079** | **~1700** | **-3379** |
### 净变化
- 新增:~1220行
- 删除:~3379行
- **净减少:~2159行(42%)**
---
*设计文档创建时间:2025-12-25*