# 建页资料库

这份文档存放建页时常用但不需要每次都手写的信息：组件用法、常用图片名、配置片段。

## 1. 常用组件用法

### 1.1 BackgroundLayer

用途：长页面背景分段渲染，适合 `bg0/bg1/bg2/bg3` 这类长活动页。

```vue
<BackgroundLayer :backgroundImages="[imageUrl('bg0'), imageUrl('bg1'), imageUrl('bg2')]" />
```

### 1.2 itemCenter

用途：背景图 + 内部绝对定位内容，是活动页最常用的图片容器。

```vue
<itemCenter
  :imgUrl="imageUrl('rewardBg')"
  style="width: 100vw; min-height: 21vw; max-height: 22vw"
  contentStyle="padding: 10vw 6vw;"
>
  <!-- 内容 -->
</itemCenter>
```

开发完成后需要稳定图片容器高度时，按图片比例补 `min-height`，并同步补接近的 `max-height`，一般可用 `max-height = min-height + 1vw`，避免图片加载出来之前容器异常撑高。

### 1.3 topUser / TopUser

用途：前三名用户展示。不同页面可能有自己的 `components/topUser.vue`，新页面可先从参考页面复制一份再微调。

```vue
<TopUser
  :avatarUrl="item.userAvatar"
  :name="item.userName"
  :distributionValue="item.amount"
/>
```

```js
import TopUser from './components/topUser.vue'
```

### 1.4 Barrage

用途：中奖用户、活动动态等弹幕。LuckyDollars 页面常用。

```vue
<Barrage :list="barrageList" />
```

```js
import Barrage from './components/Barrage.vue'
```

### 1.5 ActivityTabs

用途：图片背景标签页。适合初始页面快速搭标签切换。

```vue
<ActivityTabs
  :bg-url="imageUrl('tagsBg')"
  :indicator-url="imageUrl('tagBtBg')"
  :tabs="visibleTabs"
  :active-value="currentTab"
  @change="changeTab"
/>
```

### 1.6 ActivityLotteryGrid

用途：抽奖格子、高亮框和中心按钮区域。不同抽奖背景只改 `page.config.js` 里的布局。

```vue
<ActivityLotteryGrid
  :prizes="prizeList"
  :slot-count="pageConfig.lottery.slotCount"
  :active-index="activeIndex"
  :active-frame-url="imageUrl('prizeActiveFrame')"
  :grid-style="pageConfig.lottery.grid.style"
  :position-map="pageConfig.lottery.grid.positionMap"
  :button-grid-area="pageConfig.lottery.grid.buttonGridArea"
  :prize-render-mode="pageConfig.lottery.prizeRenderMode"
  :disabled="isRolling"
  @draw="draw(1)"
/>
```

## 2. 常用图片名清单

下面的图片名来自 `src/views/Activities` 和 `src/views/Ranking` 中常见的 `imageUrl('xxx')`、`webpUrl('xxx')`、`getImgName('xxx')` 用法。新页面不需要全部准备，按模块挑选即可。

注意：图片名大小写必须和文件名一致。项目里已有一些历史命名，例如 `RankingItem`、`RankingBottomBorder`、`rechargehidden`，这里按现有代码保留。

### 2.1 页面背景和顶部

```js
imageUrl('bg') // 页面主背景
imageUrl('bg0') // 页面背景第 0 段
imageUrl('bg1') // 页面背景第 1 段
imageUrl('bg2') // 页面背景第 2 段
imageUrl('bg3') // 页面背景第 3 段
webpUrl('pageTop') // 页面顶部 WebP 背景
imageUrl('timeBg') // 活动时间背景
imageUrl('dayBg') // 日期/天数背景
imageUrl('TimeBg') // 活动时间背景，历史大写命名
```

### 2.1.1 排行榜前几名展示区

推荐新页面使用 `rankingTopBg`，含义更明确：排行榜前几名展示区背景，不限定一定是前三名。

```js
imageUrl('rankingTopBg') // 排行榜前几名展示区背景，推荐新命名
imageUrl('topBg') // 排行榜前几名展示区背景，历史命名，不再作为页面顶部背景理解
imageUrl('top1') // 第一名头像/排名装饰
imageUrl('top2') // 第二名头像/排名装饰
imageUrl('top3') // 第三名头像/排名装饰
imageUrl('top1Bg') // 第一名独立背景
imageUrl('top2Bg') // 第二名独立背景
imageUrl('top3Bg') // 第三名独立背景
```

### 2.2 标签页和按钮

```js
imageUrl('tagsBg') // 标签页整体背景
imageUrl('tagBtBg') // 标签页选中底图
imageUrl(getImgName('rankBt')) // 排行榜未选中按钮
imageUrl(getImgName('rankBtActive')) // 排行榜选中按钮
imageUrl(getImgName('rankingBt')) // 排行按钮
imageUrl(getImgName('rankingBtActive')) // 排行选中按钮
imageUrl(getImgName('lotteryBt')) // 抽奖未选中按钮
imageUrl(getImgName('lotteryBtActive')) // 抽奖选中按钮
imageUrl(getImgName('rewardBt')) // 奖励未选中按钮
imageUrl(getImgName('rewardBtActive')) // 奖励选中按钮
imageUrl(getImgName('rechargeBt')) // 充值未选中按钮
imageUrl(getImgName('rechargeBtActive')) // 充值选中按钮
imageUrl(getImgName('redeemBt')) // 兑换按钮
imageUrl(getImgName('withdrawalBt')) // 提现未选中按钮
imageUrl(getImgName('withdrawalBtActive')) // 提现选中按钮
```

### 2.3 排行榜

```js
imageUrl('RankingMain') // 排行榜主体背景
imageUrl('RankingItem') // 排行榜列表项背景
imageUrl('rankingItemBg') // 排行榜列表项背景，另一套命名
imageUrl('RankingTopBorder') // 排行榜顶部边框
imageUrl('RankingBottomBorder') // 排行榜底部边框
imageUrl('RankingBorder') // 排行榜通用边框
imageUrl('myRankingBg') // 我的排名固定栏背景
imageUrl('rankingNumBg') // 排名数字背景
imageUrl('rankingTopBg') // 排行榜前几名展示区背景，推荐新命名
imageUrl('topBg') // 排行榜前几名展示区背景，历史命名
imageUrl('top1') // 第一名头像/排名装饰
imageUrl('top2') // 第二名头像/排名装饰
imageUrl('top3') // 第三名头像/排名装饰
imageUrl('top1Bg') // 第一名独立背景
imageUrl('top2Bg') // 第二名独立背景
imageUrl('top3Bg') // 第三名独立背景
imageUrl('top1-charm') // 魅力榜第一名装饰
imageUrl('top2-charm') // 魅力榜第二名装饰
imageUrl('top3-charm') // 魅力榜第三名装饰
imageUrl('top1-wealth') // 财富榜第一名装饰
imageUrl('top2-wealth') // 财富榜第二名装饰
imageUrl('top3-wealth') // 财富榜第三名装饰
imageUrl('top1-room') // 房间榜第一名装饰
imageUrl('top2-room') // 房间榜第二名装饰
imageUrl('top3-room') // 房间榜第三名装饰
imageUrl('myRankingBg-charm') // 魅力榜我的排名背景
imageUrl('myRankingBg-wealth') // 财富榜我的排名背景
imageUrl('myRankingBg-room') // 房间榜我的排名背景
imageUrl('RankingItem-charm') // 魅力榜列表项背景
imageUrl('RankingItem-wealth') // 财富榜列表项背景
imageUrl('RankingItem-room') // 房间榜列表项背景
imageUrl('tabSelectedBg') // 榜单分类选中底图
imageUrl('rankingSeasonBt') // 赛季榜按钮
imageUrl('rankingSeasonBtActive') // 赛季榜选中按钮
imageUrl('rankingWeeklyBt') // 周榜按钮
imageUrl('rankingWeeklyBtActive') // 周榜选中按钮
```

### 2.4 抽奖

```js
imageUrl('lottery') // 抽奖机/抽奖盘背景
imageUrl('lotteryBg') // 抽奖区域背景
imageUrl('prizeActiveFrame') // 抽奖高亮框
imageUrl('draw1BtBg') // 单抽按钮背景
imageUrl('draw5BtBg') // 五连抽按钮背景
imageUrl('draw10BtBg') // 十连抽按钮背景
imageUrl('resultBg') // 中奖结果弹窗背景
imageUrl('resultBack') // 中奖结果上一页按钮
imageUrl('resultNext') // 中奖结果下一页按钮
imageUrl('resultBt') // 中奖结果确认按钮
imageUrl('resultGoOnBt') // 继续抽奖按钮
```

### 2.5 奖励和奖品

```js
imageUrl('rewardBg') // 奖励模块背景
imageUrl('reward_banner') // 奖励横幅
imageUrl('rewards') // 奖励集合图
imageUrl('rewardBack') // 奖励上一页按钮
imageUrl('rewardNext') // 奖励下一页按钮
imageUrl(getImgName('rewardReceive')) // 奖励领取按钮
imageUrl(getImgName('rewardReceived')) // 奖励已领取按钮
imageUrl(getImgName('rewardReceiveNot')) // 奖励不可领取按钮
imageUrl(getImgName('receiveBt')) // 领取按钮
imageUrl(getImgName('receivedBt')) // 已领取按钮
imageUrl('reward_gift') // 礼物奖励图标
imageUrl('reward_frame') // 头像框奖励图标
imageUrl('reward_ride') // 座驾奖励图标
imageUrl('ticket') // 抽奖券图标
imageUrl('crystal') // 水晶/活动货币图标
imageUrl('debris') // 碎片/道具图标
imageUrl('giftBg') // 礼物背景
imageUrl('giftItemBg') // 礼物列表项背景
imageUrl('RewardRecharge100') // 充值 100 奖励图
imageUrl('RewardRecharge500') // 充值 500 奖励图
imageUrl('RewardRecharge1000') // 充值 1000 奖励图
imageUrl('RewardRecharge3000') // 充值 3000 奖励图
imageUrl('RewardRecharge5000') // 充值 5000 奖励图
imageUrl('RewardRecharge10000') // 充值 10000 奖励图
imageUrl('rechargeReward100') // 充值 100 奖励图，另一套命名
imageUrl('rechargeReward500') // 充值 500 奖励图，另一套命名
imageUrl('rechargeReward1000') // 充值 1000 奖励图，另一套命名
imageUrl('rechargeReward3000') // 充值 3000 奖励图，另一套命名
imageUrl('rechargeReward5000') // 充值 5000 奖励图，另一套命名
imageUrl('rechargeReward10000') // 充值 10000 奖励图，另一套命名
```

### 2.6 任务

```js
imageUrl('taskBtGo') // 任务去完成按钮
imageUrl('taskBtReceive') // 任务领取按钮
imageUrl('taskBtReceived') // 任务已领取按钮
imageUrl('taskGame') // 游戏任务图标
imageUrl('taskGift') // 送礼任务图标
imageUrl('taskRecharge') // 充值任务图标
imageUrl('taskTalk') // 聊天/上麦任务图标
imageUrl('taskItemBg') // 任务列表项背景
imageUrl('taskListBg') // 任务列表背景
imageUrl('taskTypeGame') // 游戏任务类型图标
imageUrl('taskTypeGift') // 礼物任务类型图标
```

### 2.7 弹窗、规则、帮助和历史

弹窗遮罩统一使用公共组件 `src/components/MaskLayer.vue`，不要在页面里重复手写固定定位遮罩、背景滚动锁定和滚动阻止逻辑。弹窗内容放入默认插槽；如果要点击空白关闭，在插槽内包一层全屏容器并绑定 `@click.self`。

```vue
<MaskLayer :maskLayerShow="showPopup">
  <div
    style="width: 100vw; height: 100vh; display: flex; align-items: flex-start; justify-content: center; padding: 6vh 0 10vh; box-sizing: border-box;"
    @click.self="showPopup = false"
  >
    <!-- 弹窗内容模块：使用 itemCenter 或普通 div 承载弹窗背景和内容 -->
  </div>
</MaskLayer>
```

```js
imageUrl(getImgName('helpInfo')) // 帮助说明图片
imageUrl('helpBg') // 帮助弹窗背景
imageUrl('helpBt') // 帮助按钮
imageUrl(getImgName('ruleInfo')) // 规则说明图片
imageUrl('ruleInfoBg') // 规则弹窗背景
imageUrl(getImgName('ruleTitle')) // 规则标题图
imageUrl(getImgName('rulesBt')) // 规则按钮
imageUrl(getImgName('historyBt')) // 历史按钮
imageUrl(getImgName('historyBg')) // 历史弹窗背景
imageUrl('historyTopBg') // 历史弹窗顶部背景
imageUrl('recordBg') // 记录列表背景
imageUrl('closeBt') // 关闭按钮
```

### 2.8 用户信息、充值和收益

```js
imageUrl('myInfoBg') // 我的信息背景
imageUrl('myAvatarFrame') // 我的头像框
imageUrl('userNameBorder') // 用户名边框
imageUrl('userFrame') // 用户头像框
imageUrl('earningInfoBg') // 收益信息背景
imageUrl('earningsBg') // 收益背景
imageUrl('distribution') // 分发/派奖图标
imageUrl('rechargehidden') // 充值金额隐藏图标
imageUrl('rechargeShow') // 充值金额显示图标
imageUrl('rechargeNumHidden') // 充值数字隐藏图标
imageUrl('rechargeNumShow') // 充值数字显示图标
imageUrl('amountHiddenBt') // 金额隐藏按钮
imageUrl('amountShowBt') // 金额显示按钮
```

### 2.9 游戏玩法和特殊模块

```js
imageUrl('gameLotteryBt') // 游戏玩法-抽奖按钮
imageUrl('gameLotteryBtActive') // 游戏玩法-抽奖选中按钮
imageUrl('gameSignInBt') // 游戏玩法-签到按钮
imageUrl('gameSignInBtActive') // 游戏玩法-签到选中按钮
imageUrl('gameExchangeBt') // 游戏玩法-兑换按钮
imageUrl('gameExchangeBtActive') // 游戏玩法-兑换选中按钮
imageUrl('goodsItem') // 兑换商品项背景
imageUrl('makeUpBtBg') // 补签按钮背景
imageUrl('rewardGoodsBg') // 奖励物品背景
imageUrl('progressBar') // 进度条图片
imageUrl('timeBack') // 时间/日期上一页按钮
imageUrl('timeNext') // 时间/日期下一页按钮
```

## 3. 模块预设

这些预设用于创建“页面创建信息 md”时自动填充，目标是少问、少填、先生成能跑的初版。图片字段优先从 `3.1 多语言图片清单` 中匹配；没有匹配到的图片名可以留空或标记待确认。

### 3.1 Activities 通用活动页

适用：`src/views/Activities` 下类似 LuckyDollars、LesserBairam、SpringFestival 的活动页。

```json
{
  "reward": {
    "enabledWhenAnyImageExists": ["rewardBg", "rewardReceive", "rewardReceived", "rewardReceiveNot"],
    "apis": ["activityRewardConfigs", "myRecharge", "receiveRechargeReward"],
    "defaultImages": ["rewardBg", "rewardReceive", "rewardReceived", "rewardReceiveNot", "ticket", "reward_gift", "reward_frame", "reward_ride"],
    "remark": "充值/消费奖励模块"
  },
  "ranking": {
    "enabledWhenAnyImageExists": ["RankingItem", "myRankingBg", "rankingTopBg", "topBg", "top1", "top2", "top3", "rankBt", "rankBtActive"],
    "apis": ["ranklist"],
    "loadMode": "remote",
    "pageSize": 20,
    "topCount": 3,
    "myRankingPath": "ranklist.body.currentUser",
    "fieldMap": {
      "avatar": "userAvatar",
      "name": "userName",
      "value": "amount",
      "userId": "userId",
      "rank": "rank"
    },
    "defaultImages": ["RankingItem", "myRankingBg", "rankingTopBg", "top1", "top2", "top3"],
    "remark": "活动内排行榜，常见为接口分页"
  },
  "lottery": {
    "enabledWhenAnyImageExists": ["lottery", "lotteryBg", "prizeActiveFrame", "lotteryBt", "lotteryBtActive"],
    "drawApis": ["onceDraw", "multiDraw"],
    "prizePoolApi": "activityDetail",
    "drawCountApi": "myTickets",
    "extraApis": ["myTotalDrawCount", "rewardConfigsBadge", "winnerHistory"],
    "historyApi": "drawRecords",
    "displayMode": "track-only",
    "slotCount": 8,
    "historyButtonPosition": "inside",
    "defaultImages": {
      "background": "lottery",
      "activeFrame": "prizeActiveFrame",
      "historyButton": "historyBt",
      "resultBackground": "resultBg"
    },
    "remark": "抽奖接口基本固定；奖品展示方式按页面图片调整"
  },
  "task": {
    "enabledWhenAnyImageExists": ["taskBtGo", "taskBtReceive", "taskBtReceived", "taskGame", "taskGift", "taskRecharge", "taskTalk"],
    "apis": ["ActTaskList", "receiveTickets"],
    "defaultImages": ["taskBtGo", "taskBtReceive", "taskBtReceived", "taskGame", "taskGift", "taskRecharge", "taskTalk"],
    "remark": "任务列表和任务领奖"
  },
  "withdraw": {
    "enabledWhenAnyImageExists": ["earningInfoBg", "myInfoBg"],
    "apis": ["withdrawableAmount", "transferActivityDollar", "exchangeCoin"],
    "defaultImages": ["earningInfoBg", "myInfoBg"],
    "remark": "提现、转账或兑换模块"
  },
  "popups": {
    "rule": {
      "enabledWhenAnyImageExists": ["rulesBt", "ruleInfo"],
      "triggerImage": "rulesBt",
      "contentImage": "ruleInfo"
    },
    "help": {
      "enabledWhenAnyImageExists": ["helpBt", "helpInfo"],
      "triggerImage": "helpBt",
      "contentImage": "helpInfo"
    },
    "history": {
      "enabledWhenAnyImageExists": ["historyBt", "historyBg"],
      "triggerImage": "historyBt",
      "backgroundImage": "historyBg",
      "apis": ["drawRecords"]
    }
  }
}
```

### 3.2 Ranking 独立排行榜页

适用：`src/views/Ranking` 下独立排行榜页。这里要特别区分加载方式：

- `remote`：接口支持 `pageNo/pageSize`，上拉时继续请求下一页。
- `local`：接口一次返回完整榜单，前端先展示一段，上拉时 `slice` 下一段，例如 `src/views/Ranking/Overall/Ranking.vue`。

```json
{
  "ranking": {
    "loadMode": "local / remote",
    "topCount": 3,
    "pageSize": 10,
    "fieldMap": {
      "avatar": "avatar",
      "name": "nickname",
      "value": "quantityFormat",
      "userId": "id",
      "rank": "rank"
    },
    "commonImages": [
      "RankingItem",
      "myRankingBg",
      "top1",
      "top2",
      "top3",
      "tabSelectedBg"
    ],
    "remark": "如果接口差异不明确，先问用户参考哪个 Ranking 页面"
  }
}
```

### 3.3 什么时候需要问用户

```text
同一模块存在多个接口版本时，问参考页面。
抽奖模块不是 8 格或不是 track-only 时，问抽奖展示方式。
排行榜接口不知道是 remote 还是 local 时，问参考页面或看接口是否传 pageNo/pageSize。
图片名看不出用途且无法从参考页面推断时，标记待确认，不阻塞初版生成。
```

## 4. 页面配置片段

### 4.1 图片工具

```js
import { useActivityAssets } from '@/composables/activity/useActivityAssets.js'
import { pageConfig } from './page.config.js'

const currentLangType = computed(() => {
  return langStore.selectedLang?.type || 'en'
})

const { imageUrl, webpUrl, getImageName: getImgName } = useActivityAssets(
  pageConfig,
  currentLangType,
)
```

### 4.2 标签页

```js
import { useActivityTabs } from '@/composables/activity/useActivityTabs.js'

const { currentTab, visibleTabs, isTab, changeTab } = useActivityTabs(pageConfig.tabs, {
  translate: t,
})
```

#### 4.2.1 嵌套标签 children

用途：描述“一级标签下还有二级标签/子区块”的页面结构，例如 Gulben Festival 的 `Personal Rank` 和 `Room Rank` 下再分 `Reward / Ranking`。

推荐配置形态：

```js
modules: {
  tabs: {
    enabled: true,
    defaultValue: 'Personal Rank',
    items: ['CollectCards to Win Prizes', 'Personal Rank', 'Room Rank'],
  },
  sections: [
    {
      name: 'personalRank',
      enabled: true,
      images: ['tagActiveBg', 'tagBg'],
      children: {
        tabs: {
          enabled: true,
          defaultValue: 'Reward',
          items: ['Reward', 'Ranking'],
        },
        sections: [
          {
            name: 'Reward',
            enabled: true,
            images: ['moduleActiveBg', 'moduleBg'],
            children: {},
          },
          {
            name: 'Ranking',
            enabled: true,
            images: ['moduleActiveBg', 'moduleBg'],
            children: {},
          },
        ],
      },
    },
  ],
}
```

生成规则：

```text
children 为空对象时，不生成子标签。
children.tabs 控制当前 section 内部的二级标签状态。
children.sections 控制当前 section 内部的二级内容。
children.tabs.defaultValue 必须能匹配 children.sections 里的 name/value。
items 是字符串时，直接用字符串作为 value 和显示文本；items 是对象时，优先使用 value、label、labelKey、buttonImage、activeButtonImage。
备注写明“开发期间不加翻译”时，页面直接展示 md 里的英文文本。
标签图片为背景时，优先用 itemCenter 或等价图片容器承载文本，不把整段文字做成图片。
```

### 4.3 排行榜

```js
import { useActivityRanking } from '@/composables/activity/useActivityRanking.js'

const rankingState = useActivityRanking({
  loadMode: pageConfig.ranking.loadMode,
  pageSize: pageConfig.ranking.pageSize,
  topCount: pageConfig.ranking.topCount,
  fetchPage: ({ pageNo, pageSize }) => {
    return ranklist({
      activityId: pageConfig.activity.activityId,
      pageNo,
      pageSize,
    })
  },
})
```

### 4.4 抽奖

```js
import { useLotteryDraw } from '@/composables/activity/useLotteryDraw.js'

const { activeIndex, isRolling, startLottery } = useLotteryDraw({
  getSlotCount: () => prizeList.value.length || pageConfig.lottery.slotCount,
})
```

### 4.5 分阶段图片预加载

用途：把首屏关键图片和后续图片拆开。首屏关键图片参与进入页面加载动画；弹窗、非默认标签页、下方长背景等图片在页面进入后后台继续预加载。

```js
import { connectToApp } from '@/utils/appConnector.js'

// 进入页面前必须完成的图片，加载完成后再结束 loading 动画。
const blockingPreloadImageNames = pageConfig.preload?.blocking || []

// 进入页面后继续后台加载的图片，不阻塞页面展示。
const backgroundPreloadImageNames = pageConfig.preload?.background || []

// 统一走 imageUrl 和 getImgName，保证本地 OSS、线上 OSS、缓存版本、多语言后缀规则一致。
const resolvePreloadUrls = (names = []) => {
  return [...new Set(names)].map((name) => imageUrl(getImgName(name)))
}

const preloadImageList = (urls = []) => {
  return Promise.all(
    urls.map((url) => new Promise((resolve) => {
      const img = new Image()
      img.onload = resolve
      img.onerror = resolve
      img.src = url
    })),
  )
}

const preloadBlockingImages = async () => {
  await preloadImageList(resolvePreloadUrls(blockingPreloadImageNames))
}

const preloadBackgroundImages = () => {
  const urls = resolvePreloadUrls(backgroundPreloadImageNames)
  if (!urls.length) return

  // 后台预加载不影响当前页面展示，所以失败也只吞掉。
  const run = () => preloadImageList(urls).catch(() => {})
  if (window.requestIdleCallback) {
    window.requestIdleCallback(run)
    return
  }
  setTimeout(run, 0)
}

// 首屏预加载：只处理阻塞图片，不等待业务接口。
const completePreloading = async () => {
  try {
    await preloadBlockingImages()
  } finally {
    isLoading.value = false
    preloadBackgroundImages()
  }
}

// 页面初始化接口：连接 App 并写入请求头后，再单独拉取业务数据。
const loadInitialPageData = async () => {
  // await getPageData()
}

// 连接 App：使用接口的页面必须在 connectToApp 成功回调后再发起业务接口请求。
const connectToAppHandler = async () => {
  await connectToApp(() => {
    loadInitialPageData()
  })
}

onMounted(() => {
  completePreloading()
  connectToAppHandler()
})
```
