# bindbox-mini 组件化重构设计

## 架构目标

将三个活动页面（yifanshang/duiduipeng/wuxianshang）共约5079行代码减少至约2500行，消除61%的冗余。

---

## 架构设计图

```mermaid
graph TB
    subgraph Utils[工具层 utils/]
        A1[activity.js<br>活动相关工具函数]
        A2[format.js<br>格式化工具]
        A3[cache.js<br>缓存管理]
    end

    subgraph Composables[组合式函数 composables/]
        B1[useActivity.js<br>活动数据管理]
        B2[useIssues.js<br>期数据管理]
        B3[useRewards.js<br>奖励数据管理]
        B4[usePayment.js<br>支付流程]
    end

    subgraph Components[组件层 components/]
        subgraph Layout[布局组件]
            C1[ActivityPageLayout.vue<br>活动页面框架]
            C2[ActivityHeader.vue<br>头部卡片]
        end
        subgraph Biz[业务组件]
            C3[ActivityTabs.vue<br>Tab切换]
            C4[RewardsPopup.vue<br>奖品弹窗]
            C5[RecordsList.vue<br>购买记录]
            C6[RewardsPreview.vue<br>奖池预览]
        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<String>`
- `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
<template>
  <ActivityPageLayout :cover-url="coverUrl">
    <template #header>
      <ActivityHeader 
        :title="detail.name" 
        :price="detail.price_draw" 
        price-unit="/发"
        :cover-url="coverUrl"
        :tags="['公开透明', '拒绝套路']"
        :scheduled-time="scheduledTimeText"
        @show-rules="showRules"
        @go-cabinet="goCabinet"
      />
    </template>

    <template #content>
      <ActivityTabs v-model="tabActive">
        <template #pool>
          <RewardsPreview :rewards="currentIssueRewards" @view-all="openRewardsPopup" />
        </template>
        <template #records>
          <RecordsList :records="winRecords" />
        </template>
      </ActivityTabs>

      <!-- 一番赏专属：选号组件 -->
      <YifanSelector ... />
    </template>

    <template #modals>
      <RewardsPopup v-model:visible="rewardsVisible" ... />
      <FlipGrid ref="flipRef" ... />
    </template>
  </ActivityPageLayout>
</template>

<script setup>
import { useActivity, useIssues, useRewards, useRecords } from '@/composables'
// 专注于一番赏特有的业务逻辑
</script>
```

---

## 文件变更清单

### 新增文件

| 文件路径 | 行数估算 | 说明 |
|----------|---------|------|
| `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*
