# 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*