Compare commits
10 Commits
ec2657be74
...
572c4cee1b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
572c4cee1b | ||
|
|
5c26f868a1 | ||
|
|
6b775b986c | ||
|
|
df36baf61d | ||
|
|
7810a1294c | ||
|
|
615a053c47 | ||
|
|
9422640aca | ||
|
|
12d78b82bd | ||
|
|
9f9bae43d6 | ||
|
|
7dd76bc7e2 |
2
.env
2
.env
@ -1,5 +1,5 @@
|
||||
# 应用版本号
|
||||
VITE_APP_VERSION=7.0.54
|
||||
VITE_APP_VERSION=7.0.58
|
||||
|
||||
# 版本类型
|
||||
# major:重大更新(清除除了白名单外的所有缓存:用户缓存、图片缓存和assets缓存)
|
||||
|
||||
@ -143,13 +143,14 @@ page.config.js 是 md 的结构化落地结果,不能脱离 md 自行新增业
|
||||
如果发现 page.config.js 或 index.vue 与 md 不一致,优先询问或按 md 同步,而不是反向猜测 md。
|
||||
生成 vue 页面时必须渐进式生成:先生成可预览底座,再按用户后续指令逐个补模块和接口。
|
||||
可预览底座只包含预加载、组件引用、图片工具、背景、基础顶部、标签容器和空内容占位,方便用户实时预览页面效果。
|
||||
新建页面只要生成 `index.vue`,必须补充统一加载动画:模板首层使用 `v-if="isLoading"` 展示居中的 `LoadingContent`,主页面内容使用 `v-else`,避免加载阶段提前挂载背景图和业务节点。`isLoading` 默认为 `true`,进入页面时至少执行一次 `completePreloading()`;即使 blocking 预加载列表为空,也要通过 `finally` 关闭 loading,防止页面卡死。
|
||||
新建页面只要生成 `index.vue`,必须补充统一加载动画:模板首层使用 `v-if="isLoading"` 展示居中的 `LoadingContent`,主页面内容使用 `v-else`,避免加载阶段提前挂载背景图和业务节点。`isLoading` 默认为 `true`,进入页面时至少执行一次 `completePreloading()`;`completePreloading()` 只负责阻塞图片预加载,不等待业务接口;即使 blocking 预加载列表为空,也要通过 `finally` 关闭 loading,防止页面卡死。
|
||||
页面只要使用接口,就必须从 `@/utils/appConnector.js` 引入 `connectToApp`,并在 `onMounted` 中先调用 `connectToAppHandler()`;所有初始化业务接口和需要请求头的接口必须放在 `connectToApp(() => { loadInitialPageData() })` 成功回调之后执行,确保 App 请求头已写入。图片预加载和接口初始化要拆成独立方法,允许并行启动,但 loading 动画只由图片预加载控制。
|
||||
业务模块不得一次性全部生成;抽奖、任务、排行榜、奖励、弹窗、接口对接都要等用户明确说生成哪一块再做。
|
||||
页面中的规则、奖励、历史、结果等弹窗模块必须统一使用公共组件 `src/components/MaskLayer.vue` 作为遮罩层和背景滚动锁定容器;弹窗内容放入默认插槽,需要点击遮罩空白关闭时,在插槽内再包一层全屏内容容器并使用 `@click.self` 关闭。
|
||||
开发预览阶段,样式优先靠近结构:能直接用内联样式表达的固定不变、一次性布局尺寸、间距、定位,直接写在模板内联样式里,方便实时调整并减少不必要的 class 拆分;可复用样式、状态样式、伪类/媒体查询/RTL 规则、复杂模块样式仍使用 class。
|
||||
动态样式不要优先使用 `:style="{ ... }"` 对象写法;涉及状态切换、选中态、方向、显隐、尺寸变化等动态效果时优先切换 class。只有必须绑定运行时数值且不适合枚举成 class 时,才使用动态 style。
|
||||
内联样式和 class 在普通静态场景下性能差异通常很小;需要避免的是大量节点重复生成动态 style 对象、频繁变更大段内联样式。开发结束或模块稳定后,再按复用性和可维护性统一抽成 class。
|
||||
使用 `itemCenter` 承载图片背景时,必须给组件写入按图片比例计算出的 `min-height`,避免图片未加载时内部信息挤在一起。计算方式:先确认组件在页面中的展示宽度(单位 vw),再读取图片实际宽高,`min-height = Math.ceil(展示宽度vw * 图片高度 / 图片宽度) + 'vw'`。如果组件放在网格或固定宽度容器里,展示宽度按该容器实际占用的 vw 计算;如果同一背景有多语言图片,取对应语言图片中最高的计算结果。
|
||||
普通业务模块一般不要提前设置 `min-height` 和 `line-height`;`min-height` 只在基础页面容器(如 `.full-page` / `.fullPage`)中按需使用。`itemCenter` 的 `min-height` 不在页面初建阶段预设,只在页面开发完成后确实需要稳定图片容器高度时再按图片比例补充;补充时先确认组件展示宽度(单位 vw),再读取图片实际宽高,`min-height = Math.ceil(展示宽度vw * 图片高度 / 图片宽度) + 'vw'`。为避免图片加载出来之前 `itemCenter` 容器高度异常撑高,补充 `min-height` 时同步补一个接近的 `max-height`,默认可按 `max-height = min-height + 1vw` 处理,例如 `min-height: 21vw; max-height: 22vw`。
|
||||
生成的 Vue 文件统一使用 `<style lang="scss" scoped>`;页面基础样式固定放在 style 块顶部,包括 `* { color: #fff; }`、`.fullPage {}`、响应式基础字号 media query。阿语等 RTL 规则也集中写在 style 块中,使用 `[dir='rtl'] xxx`,不要散在模板内联样式里。
|
||||
除基础 media query 中设置响应式字号外,页面内其它展示字号统一使用 `font-size: 1em`,不要再写 `vw`、`px` 或 `clamp()` 字号,方便所有文本跟随统一基准字号变化。
|
||||
模块配置中如果出现 `images.container` 或 `containerImages` 这类整块大背景图,生成 Vue 时统一使用 `itemCenter` 承载;容器宽度先写 `100vw`,父级用 flex 从上往下排列,`gap`、`padding`、`margin` 初始先写 `0`,不额外加边框虚线,后续由用户按预览继续调整。
|
||||
@ -167,6 +168,7 @@ page.config.js 是 md 的结构化落地结果,不能脱离 md 自行新增业
|
||||
新增 js/vue 文件尽量加中文注释。
|
||||
页面后续更新模块、接口或公共方法时,必须给新增/调整的方法和关键模块逻辑添加简短中文注释,说明用途、接口来源、字段映射或展示规则;模板里的主要 `div`、`section`、`itemCenter` 等结构模块也要补中文注释,说明当前块负责的视觉区域或业务含义;不要只堆代码,方便后续继续按模块迭代。
|
||||
页面任何模块都不要出现可见滚动条。新建或更新页面时,推荐页面根容器固定 `height: 100vh`、`overflow: hidden`,由内部主内容容器负责 `overflow-y: auto` 垂直滚动,并隐藏滚动条;需要滚动时允许内容滚动,但必须通过 `scrollbar-width: none`、`-ms-overflow-style: none` 和 `::-webkit-scrollbar { display: none; }` 隐藏可见滚动条。
|
||||
多 tab 或多分类列表如果内部包含大量固定图片、`itemCenter` 背景图、本地 OSS 图标,切换 tab 时优先使用 `v-show` 保留 tab 内容容器 DOM,避免 `v-if` 销毁重建导致固定图片反复加载;数据层仍按 tab 独立数组和独立 request token 处理,切换时可以继续重新请求接口刷新最新数据。
|
||||
docs 下文件和文件夹使用中文命名。
|
||||
页面底座以能运行、可预览、结构清楚、方便继续逐步补模块为目标。
|
||||
```
|
||||
@ -339,7 +341,7 @@ src/config/imageCacheVersion.js
|
||||
4. 两个列表都要走同一套 imageUrl/getImgName 规则,保证本地 OSS、线上 OSS、图片缓存和多语言后缀一致。
|
||||
5. 后台预加载在 blocking 完成、页面可见后执行,优先使用 requestIdleCallback,兼容时用 setTimeout。
|
||||
6. 后台预加载失败只记录日志,不影响页面展示和交互。
|
||||
7. 所有新建页面必须有统一加载动画:`isLoading=true`、`completePreloading()`、`try/catch/finally`、`<LoadingContent>{{ $t('loading') }}...</LoadingContent>`。加载态期间只渲染 loading 容器,不渲染正式页面背景、装饰图或业务模块。
|
||||
7. 所有新建页面必须有统一加载动画:`isLoading=true`、`completePreloading()`、`try/catch/finally`、`<LoadingContent>{{ $t('loading') }}...</LoadingContent>`。加载态期间只渲染 loading 容器,不渲染正式页面背景、装饰图或业务模块;`completePreloading()` 只处理图片预加载,业务接口统一提取到 `loadInitialPageData()` 等独立方法中。
|
||||
```
|
||||
|
||||
## 7. 完成后写回专属 md
|
||||
|
||||
@ -36,14 +36,15 @@ src/views/Activities/WorldCup/页面创建信息.md
|
||||
- 没填的字段,初版页面先不猜。
|
||||
- `页面创建信息.md` 是 `page.config.js` 和 `index.vue` 的生成来源;后续补需求先改 md,再同步代码。
|
||||
- 每轮可以指定同步方向:`md -> vue` 或 `vue -> md`;方向不明确且两边冲突时,先确认再改。
|
||||
- 新建页面只要生成 `index.vue`,都必须带统一加载动画:先展示 `LoadingContent`,关键图片和初始化完成后再展示正式页面。
|
||||
- 新建页面只要生成 `index.vue`,都必须带统一加载动画:先展示 `LoadingContent`,关键图片预加载完成后再展示正式页面;loading 只针对图片,不等待业务接口。
|
||||
- 页面只要使用接口,就必须引入 `connectToApp` 并先完成 App 连接;初始化接口统一放在 `connectToApp(() => { loadInitialPageData() })` 成功回调后执行,确保请求头已设置。图片预加载方法和接口初始化方法必须分离。
|
||||
- 页面根容器推荐固定 `height: 100vh`、`overflow: hidden`,内部主内容容器负责 `overflow-y: auto` 垂直滚动并隐藏滚动条。
|
||||
- 页面任何模块都不要出现可见滚动条。
|
||||
- 页面模块布局可以使用 flex column,模块间距按需求设置,常见活动页默认可用 `gap: 2vw`。
|
||||
- 能用内联样式直接表达的固定布局、尺寸、间距、定位,优先使用内联样式,方便实时调位置。
|
||||
- 可复用样式、伪类、媒体查询、RTL 规则和复杂状态样式可以写 class。
|
||||
- 动态选中态、显隐和尺寸变化可以使用方法返回内联 style 字符串;如果复用明显,再抽 class。
|
||||
- 使用 `itemCenter` 承载图片背景时,要按图片比例补 `min-height`,避免图片未加载时内部内容挤在一起。
|
||||
- 普通业务模块一般不要提前设置 `min-height` 和 `line-height`;`min-height` 只在基础页面容器(如 `.full-page` / `.fullPage`)中按需使用,`itemCenter` 的 `min-height` 只在页面开发完成后确实需要稳定图片容器高度时再补;补充时同步设置接近的 `max-height`,默认可按 `max-height = min-height + 1vw`,例如 `min-height: 21vw; max-height: 22vw`。
|
||||
- `itemCenter` 承载大背景图时,宽度优先写 `100vw`,父级从上往下 flex 排列。
|
||||
- `v-smart-img` 只用于项目自身静态图片,例如通过 `src/config/imagePaths.js` 中 `getPngUrl`、`getWebpUrl` 等方法,或活动页 `imageUrl`、`webpUrl` 等封装得到的图片地址。
|
||||
- 接口返回的远程图片链接不要使用 `v-smart-img`,直接用原生 `<img :src="接口字段">` 展示,不走本地图片缓存或智能图片指令。
|
||||
@ -53,6 +54,7 @@ src/views/Activities/WorldCup/页面创建信息.md
|
||||
- 对接 POST 接口时,如果接口没有业务参数,也必须传空对象 `{}`,不要省略 body。
|
||||
- 多 tab 或多分类列表不要共用同一个承载数组;每个 tab 使用独立数据源,避免接口慢返回或快速切换时数据混用。
|
||||
- 多 tab 列表请求建议使用每个 tab 独立的请求序号或 token,只忽略同 tab 内更旧的响应,不影响其他 tab 的缓存。
|
||||
- 多 tab 列表如果内部有大量固定图片、`itemCenter` 背景图或本地 OSS 图标,tab 内容容器优先使用 `v-show` 保留 DOM,避免每次切换时因 `v-if` 销毁重建导致固定图片反复加载;接口仍可在切换时重新请求最新数据。
|
||||
- 弹窗统一使用公共组件 `src/components/MaskLayer.vue`。规则、奖励、历史、结果等弹窗内容放在 `MaskLayer` 默认插槽内;需要点击空白关闭时,在插槽内包一层全屏容器并使用 `@click.self` 关闭。
|
||||
|
||||
## 3. 页面信息 md 模板
|
||||
|
||||
@ -17,11 +17,17 @@
|
||||
用途:背景图 + 内部绝对定位内容,是活动页最常用的图片容器。
|
||||
|
||||
```vue
|
||||
<itemCenter :imgUrl="imageUrl('rewardBg')" contentStyle="padding: 10vw 6vw;">
|
||||
<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`,新页面可先从参考页面复制一份再微调。
|
||||
@ -253,7 +259,7 @@ imageUrl('taskTypeGift') // 礼物任务类型图标
|
||||
```vue
|
||||
<MaskLayer :maskLayerShow="showPopup">
|
||||
<div
|
||||
style="width: 100vw; min-height: 100vh; display: flex; align-items: flex-start; justify-content: center; padding: 6vh 0 10vh; box-sizing: border-box;"
|
||||
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 承载弹窗背景和内容 -->
|
||||
@ -555,6 +561,8 @@ const { activeIndex, isRolling, startLottery } = useLotteryDraw({
|
||||
用途:把首屏关键图片和后续图片拆开。首屏关键图片参与进入页面加载动画;弹窗、非默认标签页、下方长背景等图片在页面进入后后台继续预加载。
|
||||
|
||||
```js
|
||||
import { connectToApp } from '@/utils/appConnector.js'
|
||||
|
||||
// 进入页面前必须完成的图片,加载完成后再结束 loading 动画。
|
||||
const blockingPreloadImageNames = pageConfig.preload?.blocking || []
|
||||
|
||||
@ -594,9 +602,30 @@ const preloadBackgroundImages = () => {
|
||||
setTimeout(run, 0)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await preloadBlockingImages()
|
||||
isLoading.value = false
|
||||
preloadBackgroundImages()
|
||||
// 首屏预加载:只处理阻塞图片,不等待业务接口。
|
||||
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()
|
||||
})
|
||||
```
|
||||
|
||||
@ -119,7 +119,7 @@ const bgVapUrl = mp4Url('bg')
|
||||
生成的线上地址形如:
|
||||
|
||||
```text
|
||||
https://cdn.azizichat.com/h5/Azizi/Activities/ActiveGame/bg.mp4
|
||||
https://asset.global-interaction.com/h5/Azizi/Activities/ActiveGame/bg.mp4
|
||||
```
|
||||
|
||||
开发环境会走:
|
||||
@ -199,7 +199,7 @@ await preloadVapConfig(resolveProtectedAssetUrl(bgVapUrl))
|
||||
|
||||
```js
|
||||
const rewardVapUrl =
|
||||
'https://cdn.azizichat.com/svgasource/manager-d824e59b-1a88-4d4f-aed3-01768ba20542.mp4'
|
||||
'https://asset.global-interaction.com/svgasource/manager-d824e59b-1a88-4d4f-aed3-01768ba20542.mp4'
|
||||
```
|
||||
|
||||
## 预加载建议
|
||||
@ -283,4 +283,3 @@ await Promise.all([
|
||||
- inline 不显示:确认父容器有 `position: relative/absolute` 和明确宽高。
|
||||
- overlay 被遮挡:检查是否有更高的 z-index;默认播放器弹层是 `3000`。
|
||||
- 背景遮挡内容:检查页面层级,ActiveGame 当前是页面背景 `z-index: 0`、VAP 背景 `z-index: 1`、页面内容 `z-index: 2`。
|
||||
|
||||
|
||||
934
docs/项目详情.md
934
docs/项目详情.md
File diff suppressed because it is too large
Load Diff
@ -21,7 +21,10 @@ export const apiPostWorldCupMatchList = async (data) => {
|
||||
// 提交世界杯比赛押注:betOption 为 1 主胜、2 平局、3 客胜。
|
||||
export const apiPostWorldCupBet = async (data) => {
|
||||
try {
|
||||
const response = await post(`${WORLD_CUP_API_PREFIX}/bet`, data)
|
||||
const response = await post(`${WORLD_CUP_API_PREFIX}/bet`, {
|
||||
...data,
|
||||
matchId: data?.matchId == null ? '' : String(data.matchId),
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
logWorldCupApiError('Failed to submit world cup bet:', error)
|
||||
@ -32,7 +35,10 @@ export const apiPostWorldCupBet = async (data) => {
|
||||
// 获取我的世界杯竞猜记录:matchId 为空时查询全部比赛记录。
|
||||
export const apiPostWorldCupMyBetList = async (data) => {
|
||||
try {
|
||||
const response = await post(`${WORLD_CUP_API_PREFIX}/my/bet/list`, data)
|
||||
const response = await post(`${WORLD_CUP_API_PREFIX}/my/bet/list`, {
|
||||
...data,
|
||||
matchId: data?.matchId == null || data.matchId === '' ? data?.matchId : String(data.matchId),
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
logWorldCupApiError('Failed to get world cup my bet list:', error)
|
||||
|
||||
@ -102,7 +102,7 @@ const shouldShowImage = computed(() => {
|
||||
// const isOSSImage = computed(() => {
|
||||
// return (
|
||||
// props.imgUrl.startsWith('/oss/h5/Azizi/') ||
|
||||
// props.imgUrl.startsWith('https://cdn.azizichat.com/h5/Azizi/')
|
||||
// props.imgUrl.startsWith('https://asset.global-interaction.com/h5/Azizi/')
|
||||
// )
|
||||
// })
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { protectAssetUrl } from '@/utils/protectedAssets.js'
|
||||
|
||||
export const OSS_REMOTE_BASE_URL = 'https://asset.global-interaction.com/h5/Azizi/'
|
||||
|
||||
export const OSS_BASE_URL =
|
||||
process.env.NODE_ENV === 'development' ? '/oss/h5/Azizi/' : 'https://cdn.azizichat.com/h5/Azizi/'
|
||||
process.env.NODE_ENV === 'development' ? '/oss/h5/Azizi/' : OSS_REMOTE_BASE_URL
|
||||
|
||||
function buildAssetUrl(imagePath, filename, extension) {
|
||||
return `${OSS_BASE_URL}${imagePath}${filename}.${extension}`
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
// src/utils/imageCacheManager.js
|
||||
import { IMAGE_CACHE_VERSION } from '@/config/imageCacheVersion'
|
||||
import { OSS_REMOTE_BASE_URL } from '@/config/imagePaths.js'
|
||||
import { resolveProtectedAssetUrl } from '@/utils/protectedAssets.js'
|
||||
import { useDebounce } from '@/utils/useDebounce'
|
||||
|
||||
@ -61,7 +62,7 @@ class ImageCacheManager {
|
||||
|
||||
let cacheName
|
||||
if (
|
||||
normalizedUrl.startsWith('https://cdn.azizichat.com/h5/Azizi/') ||
|
||||
normalizedUrl.startsWith(OSS_REMOTE_BASE_URL) ||
|
||||
normalizedUrl.includes('/oss/h5/Azizi/')
|
||||
) {
|
||||
cacheName = this.imageCacheName
|
||||
@ -309,7 +310,7 @@ class ImageCacheManager {
|
||||
|
||||
const containsAssets = normalizedUrl.includes('/assets/')
|
||||
const containsOssH5 = normalizedUrl.includes('/oss/h5/Azizi/')
|
||||
const startsWithOss = normalizedUrl.startsWith('https://cdn.azizichat.com/h5/Azizi/')
|
||||
const startsWithOss = normalizedUrl.startsWith(OSS_REMOTE_BASE_URL)
|
||||
|
||||
// 只缓存三种类型的资源
|
||||
return containsAssets || containsOssH5 || startsWithOss
|
||||
|
||||
@ -3,6 +3,7 @@ import { clearVapConfigCache, preloadVapConfig } from '@/utils/vapMp4Config.js'
|
||||
import { resolveProtectedAssetUrl } from '@/utils/protectedAssets.js'
|
||||
|
||||
const REWARD_VAP_MAX_RETRY_COUNT = 3
|
||||
const ASSET_ORIGIN = 'https://asset.global-interaction.com'
|
||||
|
||||
const isEmptySourceValue = (value) => {
|
||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
||||
@ -77,7 +78,7 @@ export function useDailyRechargeRewardVap(options = {}) {
|
||||
if (resolvedUrl.startsWith('/')) {
|
||||
return process.env.NODE_ENV === 'development'
|
||||
? `/oss${resolvedUrl}`
|
||||
: `https://cdn.azizichat.com${resolvedUrl}`
|
||||
: `${ASSET_ORIGIN}${resolvedUrl}`
|
||||
}
|
||||
|
||||
return `https://${resolvedUrl}`
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
>
|
||||
<!-- 赛事结果模块:按未开始、进行中、已结束中奖、已结束未中奖/未参与展示不同状态 -->
|
||||
<itemCenter
|
||||
style="position: relative; width: 100%; min-height: 53vw"
|
||||
style="position: relative; width: 100%; min-height: 53vw; max-height: 54vw"
|
||||
:imgUrl="pngUrl(resultStatusConfig.image)"
|
||||
contentStyle=" padding: 9vw 5vw 2vw 5vw; box-sizing: border-box; display: flex; flex-direction: column; align-items: stretch; justify-content: space-between; gap: 0vw;"
|
||||
>
|
||||
@ -200,10 +200,11 @@
|
||||
<div
|
||||
style="
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1.2;
|
||||
aspect-ratio: 1 / 1.1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
"
|
||||
>
|
||||
<img
|
||||
@ -211,13 +212,30 @@
|
||||
v-smart-img.noFade
|
||||
:src="betOption.flagUrl"
|
||||
alt=""
|
||||
style="display: block; width: 78%; aspect-ratio: 1 / 1; object-fit: contain"
|
||||
style="display: block; width: 64%; aspect-ratio: 1 / 1; object-fit: contain"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
:src="betOption.flagUrl"
|
||||
alt=""
|
||||
style="display: block; width: 78%; aspect-ratio: 1 / 1; object-fit: contain"
|
||||
style="display: block; width: 68%; aspect-ratio: 1 / 1; object-fit: contain"
|
||||
/>
|
||||
<!-- 已结束胜方标识框:根据 result 字段叠加在主胜、平局或客胜对应图片上方 -->
|
||||
<img
|
||||
v-if="isDetailBetOptionWinner(betOption.option)"
|
||||
v-smart-img.noFade
|
||||
:src="pngUrl('frameWinner')"
|
||||
alt=""
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
display: block;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@ -456,6 +474,7 @@ import {
|
||||
apiPostWorldCupBet,
|
||||
apiPostWorldCupMyBetList,
|
||||
} from '@/api/worldCup.js'
|
||||
import { connectToApp } from '@/utils/appConnector.js'
|
||||
import GeneralHeader from '@/components/GeneralHeader.vue'
|
||||
import itemCenter from '@/components/itemCenter.vue'
|
||||
import arrowBackImg from '@/assets/icon/Azizi/arrowBack.png'
|
||||
@ -481,6 +500,7 @@ const blockingImages = [
|
||||
'statusEndedfail',
|
||||
'numPeopleGold',
|
||||
'drawSign',
|
||||
'frameWinner',
|
||||
'footballSign',
|
||||
]
|
||||
|
||||
@ -517,7 +537,7 @@ const winAmount = computed(() => {
|
||||
return myBets.value.reduce((total, betItem) => total + (Number(betItem.payout) || 0), 0)
|
||||
})
|
||||
|
||||
// 详情页押注对象数据:三列分别为主胜、平局、客胜,上方为我的下注额,下方为选项奖池。
|
||||
// 详情页押注对象数据:三列分别为主胜、平局、客胜,上方为选项奖池,下方为我的下注额。
|
||||
const detailBetOptions = computed(() => [
|
||||
{
|
||||
option: 1,
|
||||
@ -681,7 +701,7 @@ const hasUserBetOnOption = (option) => {
|
||||
return getMyBetAmountSum(option) > 0
|
||||
}
|
||||
|
||||
// 押注对象样式:未开始时展示用户当前点击选中态,已开始后展示用户历史押注态。
|
||||
// 押注对象样式:未开始时只展示用户当前点击选中态,已开始后展示用户历史押注态。
|
||||
const getDetailBetOptionStyle = (option) => {
|
||||
if (canSubmitBet.value) {
|
||||
return Number(selectedBetOption.value) === Number(option)
|
||||
@ -692,6 +712,11 @@ const getDetailBetOptionStyle = (option) => {
|
||||
return hasUserBetOnOption(option) ? betOptionSelectedStyle : betOptionDefaultStyle
|
||||
}
|
||||
|
||||
// 已结束胜方标识:只有已结束赛事根据 result 字段给对应押注项叠加 frameWinner。
|
||||
const isDetailBetOptionWinner = (option) => {
|
||||
return Number(matchInfo.value.status) === 2 && Number(matchInfo.value.result) === Number(option)
|
||||
}
|
||||
|
||||
// 点击押注对象:仅未开始赛事允许选择押注对象,默认不选中任何项。
|
||||
const handleBetOptionClick = (option) => {
|
||||
if (!canSubmitBet.value) return
|
||||
@ -733,26 +758,39 @@ const loadMyBetList = async () => {
|
||||
myBets.value = records
|
||||
}
|
||||
|
||||
// 页面初始化:遵循建页规则,预加载关键图片并拉取详情与我的竞猜记录后再关闭 loading。
|
||||
const completePreloading = async () => {
|
||||
// 页面初始化接口:连接 App 并写入请求头后,再独立拉取赛事详情和我的竞猜记录。
|
||||
const loadInitialEventDetailData = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
preloadImages(blockingImages.map((imageName) => pngUrl(imageName))),
|
||||
loadMatchDetail(),
|
||||
])
|
||||
await loadMatchDetail()
|
||||
await loadMyBetList()
|
||||
} catch (error) {
|
||||
console.error('WorldCup 赛事详情加载失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 首屏预加载:只处理阻塞图片,不等待接口数据返回。
|
||||
const completePreloading = async () => {
|
||||
try {
|
||||
await preloadImages(blockingImages.map((imageName) => pngUrl(imageName)))
|
||||
} catch (error) {
|
||||
console.error('WorldCup 赛事详情图片预加载失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 连接 App 并设置接口请求头:详情页所有接口请求必须在 connectToApp 成功后执行。
|
||||
const connectToAppHandler = async () => {
|
||||
await connectToApp(() => {
|
||||
loadInitialEventDetailData()
|
||||
})
|
||||
}
|
||||
|
||||
// 押注接口占位:后续第三模块接入押注按钮时统一调用该方法。
|
||||
const submitWorldCupBet = async (betOption, betAmount) => {
|
||||
if (!matchId.value) return
|
||||
return apiPostWorldCupBet({
|
||||
matchId: Number(matchId.value),
|
||||
matchId: String(matchId.value),
|
||||
betOption,
|
||||
betAmount,
|
||||
})
|
||||
@ -786,6 +824,7 @@ onMounted(() => {
|
||||
nowTime.value = Date.now()
|
||||
}, 1000)
|
||||
completePreloading()
|
||||
connectToAppHandler()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@ -7,8 +7,9 @@
|
||||
- 页面模块、div 结构、方法、接口处理都需要补充中文注释。
|
||||
- 页面根容器使用 `100vh` 方案,内部滚动并隐藏滚动条,不展示可见滚动条。
|
||||
- 能使用内联样式处理的页面样式,优先使用内联样式。
|
||||
- 新建或更新 `index.vue` 时必须使用统一加载动画:`isLoading` 阶段只渲染 `LoadingContent`,关键图片和初始化接口完成后再渲染正式背景图与业务节点。
|
||||
- 新建或更新 `index.vue` 时必须使用统一加载动画:`isLoading` 阶段只渲染 `LoadingContent`,阻塞图片预加载完成后再渲染正式背景图与业务节点;业务接口不参与 loading 阻塞。
|
||||
- 普通业务模块不提前设置 `min-height` 和 `line-height`;`min-height` 只在基础页面容器按需使用,`itemCenter` 的 `min-height` 只在页面开发完成后确实需要稳定图片容器高度时再补。
|
||||
- 页面使用接口前必须先调用 `connectToApp` 设置请求头;当前详情页的 `completePreloading()` 只负责阻塞图片预加载并控制 loading,赛事详情和我的竞猜记录接口已提取到 `loadInitialEventDetailData()`,在 `connectToAppHandler()` 连接成功后独立请求。
|
||||
|
||||
## 1. 基础信息
|
||||
|
||||
@ -20,6 +21,7 @@
|
||||
- 路由 name:`world-cup-event-detail`
|
||||
- 进入来源:WorldCup 首页赛事列表项下方的 `TOTAL PRIZE POOL` 模块。
|
||||
- Query 参数:`matchId`,来自比赛列表接口返回的 `matchId`。
|
||||
- `matchId` 必须按字符串处理和传参,禁止使用 `Number(matchId)`;比赛 ID 可能超过 JS 安全整数范围,转数字会丢失精度。
|
||||
|
||||
## 2. 业务规则
|
||||
|
||||
@ -41,7 +43,7 @@
|
||||
- 第一模块为赛事结果模块,使用 `itemCenter` 组件承载不同状态背景图。
|
||||
- 第二模块为押注模块,负责展示押注对象、押注状态和未开赛时的押注金额入口。
|
||||
- 第三模块为押注记录模块,负责展示当前赛事内用户押注对象、金额和押注时间。
|
||||
- 本页 `itemCenter` 的 `min-height` 按 `Math.ceil(实际展示宽度vw * 图片高度 / 图片宽度)` 计算;赛事结果模块在主内容区左右 `2vw` padding 后实际宽度为 `96vw`,`statusNotStarted` / `statusInProgress` / `statusEndedWin` / `statusEndedfail` 图片比例一致,计算后设置 `min-height: 53vw`。
|
||||
- 本页 `itemCenter` 的 `min-height` 按 `Math.ceil(实际展示宽度vw * 图片高度 / 图片宽度)` 计算;赛事结果模块在主内容区左右 `2vw` padding 后实际宽度为 `96vw`,`statusNotStarted` / `statusInProgress` / `statusEndedWin` / `statusEndedfail` 图片比例一致,计算后设置 `min-height: 53vw; max-height: 54vw`,避免图片加载出来之前容器高度异常撑高。
|
||||
|
||||
## 4. 赛事结果模块
|
||||
|
||||
@ -53,7 +55,7 @@
|
||||
- 开赛时间文案在所有状态下固定展示,文案根据 `kickoffTime` 格式化,例如 `Kick-off is at 18:00:00 on 12 June 2026.`。
|
||||
- 分割线和底部信息行在所有状态下固定展示;底部信息行左侧展示 `total prize pool:`、coin 图片和 `totalPool`,右侧展示 `numPeopleGold` 图片和 `joinCount`。
|
||||
- 中奖判断来自 `myBets`,`betStatus === 1` 或 `payout > 0` 都视为中奖。
|
||||
- 当前页面会预加载 `detailPageBg`、`statusNotStarted`、`statusInProgress`、`statusEndedWin`、`statusEndedfail`、`numPeopleGold`。
|
||||
- 当前页面会预加载 `detailPageBg`、`statusNotStarted`、`statusInProgress`、`statusEndedWin`、`statusEndedfail`、`numPeopleGold`、`drawSign`、`frameWinner`、`footballSign`。
|
||||
|
||||
## 5. 押注模块
|
||||
|
||||
@ -63,10 +65,11 @@
|
||||
- 押注对象上方金额展示当前选项奖池:`homePoolFormat` / `drawPoolFormat` / `awayPoolFormat`,没有格式化字段时回退 `homePool` / `drawPool` / `awayPool`。
|
||||
- 押注对象下方金额展示用户在当前选项的累计下注额:`myHomeBetFormat` / `myDrawBetFormat` / `myAwayBetFormat`,详情接口缺失时按 `myBets` 中 `betOption` 汇总兜底。
|
||||
- 押注对象金额上下行只对调字段展示,样式以当前详情页实现为准,不和首页 `Ended` 列表样式强行合并。
|
||||
- 未开始赛事中,押注对象允许点击选择,默认不选中任何项;选中的押注项使用首页 `Ended` 选中态样式。
|
||||
- 未开始赛事中,押注对象允许点击选择,默认不选中任何项;即使上方展示了用户在每个选项已押注金币数,也不会自动使用选中态,选中态仅用于用户当前点击选择的一个押注项,方便继续点击下方金币金额下注。
|
||||
- 已开始或已结束赛事中,押注对象不可点击,选中态用于展示用户已押注过的选项。
|
||||
- 已结束赛事中,根据接口 `result` 字段判断胜方或平局结果,`1` 主胜、`2` 平局、`3` 客胜,并在对应押注项图片容器内叠加 `frameWinner`;该用法和首页 `Ended` 列表项保持一致。
|
||||
- 押注金额模块只在未开始赛事展示,金额选项为 `100`、`1000`、`10000`、`100000`,每个金额左侧展示 coin 图片。
|
||||
- 押注流程为:先选择押注对象,再点击押注金额;点击金额时调用 `apiPostWorldCupBet({ matchId, betOption, betAmount })`。
|
||||
- 押注流程为:先选择押注对象,再点击押注金额;点击金额时调用 `apiPostWorldCupBet({ matchId: String(matchId), betOption, betAmount })`。
|
||||
- 押注成功后清空当前选中押注对象,并重新拉取 `apiGetWorldCupMatchDetail` 和 `apiPostWorldCupMyBetList` 刷新页面状态。
|
||||
|
||||
## 6. 押注记录模块
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
<itemCenter
|
||||
:imgUrl="charmLevelUrl"
|
||||
class="level-badge"
|
||||
style="width: 30%; min-height: 4vw"
|
||||
style="width: 30%; min-height: 4vw; max-height: 5vw"
|
||||
contentStyle="padding: 0 0 0 42%; align-items: center; justify-content: center; box-sizing: border-box;"
|
||||
>
|
||||
<span>{{ charmLevel || 0 }}</span>
|
||||
@ -74,7 +74,7 @@
|
||||
<itemCenter
|
||||
:imgUrl="wealthLevelUrl"
|
||||
class="level-badge"
|
||||
style="width: 30%; min-height: 4vw"
|
||||
style="width: 30%; min-height: 4vw; max-height: 5vw"
|
||||
contentStyle="padding: 0 0 0 36%; align-items: center; justify-content: center; box-sizing: border-box;"
|
||||
>
|
||||
<span>{{ wealthLevel || 0 }}</span>
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
|
||||
<!-- 头部模块:使用 bg 大图承载,内部放置右侧规则按钮 -->
|
||||
<itemCenter
|
||||
style="width: 100vw; min-height: 151vw"
|
||||
style="width: 100vw; min-height: 151vw; max-height: 152vw"
|
||||
:imgUrl="pngUrl('bg')"
|
||||
contentStyle="padding: 87vw 0 0; align-items: flex-start; justify-content: flex-start; box-sizing: border-box;"
|
||||
>
|
||||
@ -87,7 +87,7 @@
|
||||
|
||||
<!-- 排行榜主图区:RankingMain 内展示前三名头像框 -->
|
||||
<itemCenter
|
||||
style="width: 100vw; min-height: 92vw"
|
||||
style="width: 100vw; min-height: 92vw; max-height: 93vw"
|
||||
:imgUrl="pngUrl('RankingMain')"
|
||||
contentStyle="direction: ltr; padding: 0; flex-direction: column; align-items: center; justify-content: space-between; box-sizing: border-box;"
|
||||
>
|
||||
@ -212,7 +212,7 @@
|
||||
<itemCenter
|
||||
v-for="(rankingItem, index) in rankingListAfterTop3"
|
||||
:key="rankingItem.userId || index"
|
||||
style="width: 100vw; min-height: 23vw; margin-top: -2vw"
|
||||
style="width: 100vw; min-height: 23vw; max-height: 24vw; margin-top: -2vw"
|
||||
:imgUrl="pngUrl('RankingItem')"
|
||||
contentStyle="padding: 3vw 8vw 0; box-sizing: border-box; justify-content: space-between;"
|
||||
@click="handleRankingClick(rankingItem)"
|
||||
@ -261,7 +261,7 @@
|
||||
/>
|
||||
<itemCenter
|
||||
:imgUrl="pngUrl(getLevelBgImageName('userLevel', rankingItem.charmLevel))"
|
||||
style="width: 12vw; min-height: 7vw"
|
||||
style="width: 12vw; min-height: 7vw; max-height: 8vw"
|
||||
contentStyle="direction: ltr;padding: 0 0 0 42%; align-items: center; justify-content: center; box-sizing: border-box;"
|
||||
>
|
||||
<span style="color: #fff; font-size: 1em; font-weight: 700; line-height: 1">
|
||||
@ -270,7 +270,7 @@
|
||||
</itemCenter>
|
||||
<itemCenter
|
||||
:imgUrl="pngUrl(getLevelBgImageName('wealthLevel', rankingItem.wealthLevel))"
|
||||
style="width: 12vw; min-height: 6vw"
|
||||
style="width: 12vw; min-height: 6vw; max-height: 7vw"
|
||||
contentStyle="direction: ltr;padding: 0 0 0 36%; align-items: center; justify-content: center; box-sizing: border-box;"
|
||||
>
|
||||
<span style="color: #fff; font-size: 1em; font-weight: 700; line-height: 1">
|
||||
@ -350,7 +350,7 @@
|
||||
<section style="width: 100vw">
|
||||
<!-- 赛事列表背景容器:使用 EventlistBg 承载切换按钮和比赛列表 -->
|
||||
<itemCenter
|
||||
style="width: 100vw; min-height: 244vw"
|
||||
style="width: 100vw; min-height: 244vw; max-height: 245vw"
|
||||
:imgUrl="pngUrl('EventlistBg')"
|
||||
contentStyle="display: flex; flex-direction: column; justify-content: flex-start; align-items: center; gap: 2vw; padding: 18vw 8vw 12vw; box-sizing: border-box;"
|
||||
>
|
||||
@ -395,8 +395,11 @@
|
||||
"
|
||||
>
|
||||
<!-- 赛事列表项:Predict Now 和 Ended 使用独立数据源,避免切换时列表数据串用 -->
|
||||
<!-- Predict Now 列表项:Ended 列表后续展示其他结构,当前 tab=1 时不渲染列表项 -->
|
||||
<template v-if="activeMatchTab === 0">
|
||||
<!-- Predict Now 列表容器:使用 v-show 保留 DOM,减少固定图片在 tab 切换时反复加载 -->
|
||||
<div
|
||||
v-show="activeMatchTab === 0"
|
||||
style="width: 100%; display: flex; flex-direction: column; gap: 2vw"
|
||||
>
|
||||
<template v-for="matchItem in predictMatchList" :key="matchItem.matchId">
|
||||
<!-- Predict Now 新列表项:上方为原赛事卡片,下方为总奖池入口 -->
|
||||
<div
|
||||
@ -409,7 +412,7 @@
|
||||
"
|
||||
>
|
||||
<itemCenter
|
||||
style="width: 100%; min-height: 52vw"
|
||||
style="width: 100%; min-height: 52vw; max-height: 53vw"
|
||||
:imgUrl="pngUrl('matchBg')"
|
||||
contentStyle="width: 100%; height: 100%; padding: 0; box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between; align-items: center;"
|
||||
>
|
||||
@ -643,10 +646,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Ended 列表项:使用独立卡片样式展示已结束赛事,不使用 matchBg 背景组件 -->
|
||||
<template v-if="activeMatchTab === 1">
|
||||
<!-- Ended 列表容器:使用 v-show 保留 DOM,避免 numPeople、rightBt、drawSign 等固定图片重复加载 -->
|
||||
<div
|
||||
v-show="activeMatchTab === 1"
|
||||
style="width: 100%; display: flex; flex-direction: column; gap: 2vw"
|
||||
>
|
||||
<template v-for="matchItem in endedMatchList" :key="matchItem.matchId">
|
||||
<!-- Ended 新列表项:上方为已结束赛事卡片,下方为总奖池入口 -->
|
||||
<div
|
||||
@ -752,10 +759,11 @@
|
||||
<div
|
||||
style="
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1.2;
|
||||
aspect-ratio: 1 / 1.1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
"
|
||||
>
|
||||
<img
|
||||
@ -765,7 +773,7 @@
|
||||
alt=""
|
||||
style="
|
||||
display: block;
|
||||
width: 78%;
|
||||
width: 68%;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
"
|
||||
@ -776,11 +784,28 @@
|
||||
alt=""
|
||||
style="
|
||||
display: block;
|
||||
width: 78%;
|
||||
width: 70%;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
"
|
||||
/>
|
||||
<!-- 胜方标识框:根据 result 字段叠加在主胜、平局或客胜对应图片上方 -->
|
||||
<img
|
||||
v-if="isEndedBetOptionWinner(matchItem, betOption.option)"
|
||||
v-smart-img.noFade
|
||||
:src="pngUrl('frameWinner')"
|
||||
alt=""
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
display: block;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
@ -884,7 +909,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</itemCenter>
|
||||
</section>
|
||||
@ -896,7 +921,7 @@
|
||||
<!-- 我的排名贴底模块:固定在视口底部,展示当前用户排名信息 -->
|
||||
<itemCenter
|
||||
v-if="!isLoading"
|
||||
style="position: fixed; left: 0; bottom: 0; z-index: 10; width: 100vw; min-height: 21vw"
|
||||
style="position: fixed; left: 0; bottom: 0; z-index: 10; width: 100vw; min-height: 21vw; max-height: 22vw"
|
||||
:imgUrl="pngUrl('myRankingBg')"
|
||||
contentStyle="padding: 2vw 8vw; box-sizing: border-box; justify-content: space-between;"
|
||||
>
|
||||
@ -941,7 +966,7 @@
|
||||
/>
|
||||
<itemCenter
|
||||
:imgUrl="pngUrl(getLevelBgImageName('userLevel', myRanking.charmLevel))"
|
||||
style="width: 12vw; min-height: 7vw; margin: 0; padding: 0"
|
||||
style="width: 12vw; min-height: 7vw; max-height: 8vw; margin: 0; padding: 0"
|
||||
contentStyle="padding: 0 0 0 5vw; align-items: center; justify-content: center;"
|
||||
>
|
||||
<div style="color: #fff; font-size: 1em; font-weight: 700; line-height: 1">
|
||||
@ -950,7 +975,7 @@
|
||||
</itemCenter>
|
||||
<itemCenter
|
||||
:imgUrl="pngUrl(getLevelBgImageName('wealthLevel', myRanking.wealthLevel))"
|
||||
style="width: 12vw; min-height: 6vw; margin: 0; padding: 0"
|
||||
style="width: 12vw; min-height: 6vw; max-height: 7vw; margin: 0; padding: 0"
|
||||
contentStyle="padding: 0 0 0 4vw; align-items: center; justify-content: center;"
|
||||
>
|
||||
<div style="color: #fff; font-size: 1em; font-weight: 700; line-height: 1">
|
||||
@ -995,14 +1020,14 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 6vh 0 10vh;
|
||||
padding: 10vh 0;
|
||||
box-sizing: border-box;
|
||||
"
|
||||
@click.self="handleCloseRewardPopup"
|
||||
>
|
||||
<!-- 奖励弹窗内容:rewardBg 靠上展示,上下保留遮罩空白用于点击退出 -->
|
||||
<itemCenter
|
||||
style="width: 100vw; min-height: 225vw"
|
||||
style="width: 100vw; min-height: 225vw; max-height: 226vw"
|
||||
:imgUrl="pngUrl('rewardBg')"
|
||||
contentStyle="display: flex; flex-direction: column;justify-content: flex-start; gap: 12.5vw; padding: 34vw 6vw 0vw; box-sizing: border-box;"
|
||||
>
|
||||
@ -1147,14 +1172,14 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 6vh 0 10vh;
|
||||
padding: 10vh 0;
|
||||
box-sizing: border-box;
|
||||
"
|
||||
@click.self="handleCloseRulePopup"
|
||||
>
|
||||
<!-- 规则弹窗内容:ruleBg 高度较长,宽度占满 100vw,顶部和底部空白用于点击退出 -->
|
||||
<itemCenter
|
||||
style="width: 100vw; min-height: 178vw"
|
||||
style="width: 100vw; min-height: 178vw; max-height: 179vw"
|
||||
:imgUrl="pngUrl('ruleBg')"
|
||||
contentStyle="padding: 0; box-sizing: border-box;"
|
||||
/>
|
||||
@ -1170,6 +1195,7 @@ import { getPngUrl } from '@/config/imagePaths.js'
|
||||
import { preloadImages } from '@/utils/image/imagePreloader.js'
|
||||
import { apiPostWorldCupMatchList, apiPostWorldCupRankList } from '@/api/worldCup.js'
|
||||
import { viewUserInfo } from '@/utils/appBridge.js'
|
||||
import { connectToApp } from '@/utils/appConnector.js'
|
||||
import { handleAvatarImageError, handleRewardImageError } from '@/utils/image/imageHandler.js'
|
||||
import { formatRewardDisplay } from '@/utils/rewardFormatter.js'
|
||||
import MaskLayer from '@/components/MaskLayer.vue'
|
||||
@ -1218,6 +1244,7 @@ const blockingImages = [
|
||||
'EventlistBg',
|
||||
'matchBg',
|
||||
'drawSign',
|
||||
'frameWinner',
|
||||
'numPeople',
|
||||
'rightBt',
|
||||
'RankingMain',
|
||||
@ -1370,7 +1397,7 @@ const normalizeRankingItem = (item = {}) => {
|
||||
// 赛事列表字段归一化:只按照 apiPostWorldCupMatchList 当前返回字段生成页面数据。
|
||||
const normalizeMatchItem = (item = {}) => {
|
||||
return {
|
||||
matchId: item.matchId ?? '',
|
||||
matchId: item.matchId == null ? '' : String(item.matchId),
|
||||
homeTeam: item.homeTeam ?? '',
|
||||
awayTeam: item.awayTeam ?? '',
|
||||
homeFlag: item.homeFlag ?? '',
|
||||
@ -1544,6 +1571,11 @@ const isEndedBetOptionSelected = (matchItem = {}, option) => {
|
||||
return selectedBetAmount !== null && selectedBetAmount !== undefined && selectedBetAmount !== ''
|
||||
}
|
||||
|
||||
// Ended 胜方标识:根据 result 字段给主胜、平局或客胜对应押注项叠加 frameWinner。
|
||||
const isEndedBetOptionWinner = (matchItem = {}, option) => {
|
||||
return Number(matchItem.result) === Number(option)
|
||||
}
|
||||
|
||||
// Ended 押注项样式:用户押注过的选项展示选中态,未押注选项展示默认样式。
|
||||
const getEndedBetOptionStyle = (matchItem = {}, option) => {
|
||||
return isEndedBetOptionSelected(matchItem, option)
|
||||
@ -1678,23 +1710,32 @@ const handleCloseRulePopup = () => {
|
||||
showRulePopup.value = false
|
||||
}
|
||||
|
||||
// 首屏图片、排行榜和赛事数据一起完成后再关闭 loading。
|
||||
// 页面初始化接口:连接 App 并写入请求头后,再独立拉取排行榜和默认赛事列表。
|
||||
const loadInitialWorldCupData = async () => {
|
||||
await Promise.all([getPersonalRanking(), getWorldCupMatchList()])
|
||||
}
|
||||
|
||||
// 首屏预加载:只处理阻塞图片,不等待接口数据返回。
|
||||
const completePreloading = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
getPersonalRanking(),
|
||||
getWorldCupMatchList(),
|
||||
preloadImages(blockingImages.map((imageName) => pngUrl(imageName))),
|
||||
])
|
||||
await preloadImages(blockingImages.map((imageName) => pngUrl(imageName)))
|
||||
} catch (error) {
|
||||
console.error('WorldCup 预加载过程中发生错误:', error)
|
||||
console.error('WorldCup 图片预加载过程中发生错误:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 连接 App 并设置接口请求头:所有业务接口必须在 connectToApp 成功后再请求。
|
||||
const connectToAppHandler = async () => {
|
||||
await connectToApp(() => {
|
||||
loadInitialWorldCupData()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
completePreloading()
|
||||
connectToAppHandler()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -119,7 +119,7 @@ Body 参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"matchId": 0,
|
||||
"matchId": "0",
|
||||
"betOption": 0,
|
||||
"betAmount": 0
|
||||
}
|
||||
@ -129,7 +129,7 @@ Body 参数:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| matchId | 是 | 比赛 ID |
|
||||
| matchId | 是 | 比赛 ID,必须使用字符串格式传参,避免大整数精度丢失 |
|
||||
| betOption | 是 | 押注选项:`1` 主胜,`2` 平局,`3` 客胜 |
|
||||
| betAmount | 是 | 押注金额:`100` / `1000` / `10000` / `100000` |
|
||||
|
||||
@ -146,7 +146,7 @@ Body 参数:
|
||||
{
|
||||
"size": 0,
|
||||
"current": 0,
|
||||
"matchId": 0
|
||||
"matchId": "0"
|
||||
}
|
||||
```
|
||||
|
||||
@ -156,7 +156,7 @@ Body 参数:
|
||||
| --- | --- | --- |
|
||||
| size | 是 | 每页数量 |
|
||||
| current | 是 | 当前页 |
|
||||
| matchId | 否 | 比赛 ID,为空查全部 |
|
||||
| matchId | 否 | 比赛 ID,为空查全部;有值时必须使用字符串格式传参,避免大整数精度丢失 |
|
||||
|
||||
返回示例:
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
- 能使用内联样式处理的页面样式,优先使用内联样式。
|
||||
- 页面根容器使用 `height: 100vh`,内部主内容垂直滚动并隐藏滚动条,页面任何模块都不展示可见滚动条。
|
||||
- 接口返回的远程图片链接不要使用 `v-smart-img`,直接使用原生 `<img :src="接口字段">` 展示,不走本地图片缓存。
|
||||
- 页面使用接口前必须先调用 `connectToApp` 设置请求头;当前首页的 `completePreloading()` 只负责阻塞图片预加载并控制 loading,排行榜和赛事列表接口已提取到 `loadInitialWorldCupData()`,在 `connectToAppHandler()` 连接成功后独立请求。
|
||||
|
||||
## 1. 基础信息
|
||||
|
||||
@ -62,6 +63,7 @@
|
||||
| EventlistBg | 赛事列表模块背景 |
|
||||
| matchBg | Predict Now 赛事列表项背景 |
|
||||
| drawSign | Ended 平局押注项图片 |
|
||||
| frameWinner | Ended 列表胜方或平局结果叠加框 |
|
||||
| numPeople | 赛事列表右上角参与人数图标 |
|
||||
| rightBt | 总奖池入口右侧箭头 |
|
||||
| vip1-vip6 | VIP 等级图片 |
|
||||
@ -70,27 +72,27 @@
|
||||
|
||||
### 3.2 阻塞预加载图片
|
||||
|
||||
`bg2`、`bg`、`ruleBt`、`ruleBg`、`EventlistBg`、`matchBg`、`drawSign`、`numPeople`、`rightBt`、`RankingMain`、`RankingItem`、`RankingBottomBorder`、`myRankingBg`、`giftBt`、`rewardBg`、`showMoreBt`、`frameTop1`、`frameTop2`、`frameTop3`、`vip1-vip6`、`userLevel1Bg-userLevel5Bg`、`wealthLevel1Bg-wealthLevel5Bg`。
|
||||
`bg2`、`bg`、`ruleBt`、`ruleBg`、`EventlistBg`、`matchBg`、`drawSign`、`frameWinner`、`numPeople`、`rightBt`、`RankingMain`、`RankingItem`、`RankingBottomBorder`、`myRankingBg`、`giftBt`、`rewardBg`、`showMoreBt`、`frameTop1`、`frameTop2`、`frameTop3`、`vip1-vip6`、`userLevel1Bg-userLevel5Bg`、`wealthLevel1Bg-wealthLevel5Bg`。
|
||||
|
||||
页面进入时必须展示统一加载动画,阻塞预加载完成后再展示主内容。
|
||||
页面进入时必须展示统一加载动画,阻塞图片预加载完成后再展示主内容;业务接口不参与 loading 阻塞。
|
||||
|
||||
### 3.3 itemCenter 最小高度
|
||||
### 3.3 itemCenter 高度约束
|
||||
|
||||
`itemCenter` 的 `min-height` 按 `Math.ceil(实际展示宽度vw * 图片高度 / 图片宽度)` 计算,当前页面已补充以下值:
|
||||
`itemCenter` 的 `min-height` 按 `Math.ceil(实际展示宽度vw * 图片高度 / 图片宽度)` 计算;为避免图片加载出来之前容器高度异常撑高,同步设置接近的 `max-height`,当前页面默认按 `max-height = min-height + 1vw` 补充:
|
||||
|
||||
| 使用位置 | 图片 | 实际展示宽度 | min-height |
|
||||
| --- | --- | --- | --- |
|
||||
| 头部模块 | `bg` | `100vw` | `151vw` |
|
||||
| 排行榜主图区 | `RankingMain` | `100vw` | `92vw` |
|
||||
| 第四名以后排行榜项 | `RankingItem` | `100vw` | `23vw` |
|
||||
| 首页魅力等级徽章 | `userLevel1Bg-userLevel5Bg` | `12vw` | `7vw` |
|
||||
| 首页财富等级徽章 | `wealthLevel1Bg-wealthLevel5Bg` | `12vw` | `6vw` |
|
||||
| 赛事列表模块 | `EventlistBg` | `100vw` | `244vw` |
|
||||
| Predict Now 赛事卡片 | `matchBg` | `84vw`,来自 `EventlistBg` 内容区左右 `8vw` padding 后的可用宽度 | `52vw` |
|
||||
| 我的排名贴底模块 | `myRankingBg` | `100vw` | `21vw` |
|
||||
| 奖励弹窗 | `rewardBg` | `100vw` | `225vw` |
|
||||
| 规则弹窗 | `ruleBg` | `100vw` | `178vw` |
|
||||
| `components/topUser.vue` 等级徽章 | `userLevel*Bg` / `wealthLevel*Bg` | 当前父级最大 `26vw` 的 `30%` | `4vw` |
|
||||
| 使用位置 | 图片 | 实际展示宽度 | min-height | max-height |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 头部模块 | `bg` | `100vw` | `151vw` | `152vw` |
|
||||
| 排行榜主图区 | `RankingMain` | `100vw` | `92vw` | `93vw` |
|
||||
| 第四名以后排行榜项 | `RankingItem` | `100vw` | `23vw` | `24vw` |
|
||||
| 首页魅力等级徽章 | `userLevel1Bg-userLevel5Bg` | `12vw` | `7vw` | `8vw` |
|
||||
| 首页财富等级徽章 | `wealthLevel1Bg-wealthLevel5Bg` | `12vw` | `6vw` | `7vw` |
|
||||
| 赛事列表模块 | `EventlistBg` | `100vw` | `244vw` | `245vw` |
|
||||
| Predict Now 赛事卡片 | `matchBg` | `84vw`,来自 `EventlistBg` 内容区左右 `8vw` padding 后的可用宽度 | `52vw` | `53vw` |
|
||||
| 我的排名贴底模块 | `myRankingBg` | `100vw` | `21vw` | `22vw` |
|
||||
| 奖励弹窗 | `rewardBg` | `100vw` | `225vw` | `226vw` |
|
||||
| 规则弹窗 | `ruleBg` | `100vw` | `178vw` | `179vw` |
|
||||
| `components/topUser.vue` 等级徽章 | `userLevel*Bg` / `wealthLevel*Bg` | 当前父级最大 `26vw` 的 `30%` | `4vw` | `5vw` |
|
||||
|
||||
## 4. 页面结构
|
||||
|
||||
@ -141,11 +143,13 @@
|
||||
- 每次切换按钮都重新发送接口,暂不做上拉加载。
|
||||
- 两个 tab 的数据必须分开承载:`predictMatchList` 保存 Predict Now 数据,`endedMatchList` 保存 Ended 数据,避免接口慢返回时两个列表数据混用。
|
||||
- 请求保护使用每个 tab 独立的 request token:只忽略同 tab 内更旧的响应,不影响另一个 tab 的缓存。
|
||||
- 赛事列表两个 tab 的列表容器使用 `v-show` 保留 DOM,不使用 `v-if` 销毁重建;切换时仍重新请求接口获取最新数据,但固定图片如 `matchBg`、`numPeople`、`rightBt`、`drawSign` 不随 tab 切换反复重建加载。
|
||||
- Predict Now 使用 `matchBg` 的 `itemCenter` 渲染列表项。
|
||||
- Ended 不使用 `matchBg`,直接使用普通 `div` 卡片样式渲染列表项。
|
||||
- Predict Now 和 Ended 的单条列表都使用新的 column wrapper:上方是原赛事卡片,下方是总奖池入口,用 `gap` 控制上下间距。
|
||||
- 总奖池入口样式为金色描边渐变背景,内部一行展示 `TOTAL PRIZE POOL`、coin 图片、总奖池金额和右侧 `rightBt`;金额优先展示 `totalPoolFormat`,没有格式化字段时回退 `totalPool`。
|
||||
- 点击总奖池入口进入 `/activities/world-cup/event-detail?matchId=...`,详情页目录为 `src/views/Activities/WorldCup/EventDetail`。
|
||||
- 赛事 `matchId` 必须按字符串保留和传递,禁止使用 `Number(matchId)`;比赛 ID 可能超过 JS 安全整数范围,转数字会丢失精度。
|
||||
|
||||
Predict Now 列表项规则:
|
||||
|
||||
@ -178,6 +182,7 @@ Ended 列表项规则:
|
||||
- 平局展示 `drawSign`、`DRAW`、`drawPercent`。
|
||||
- 客方展示 `awayFlag`、`awayTeam`、`awayPercent`。
|
||||
- Ended 押注项旗帜图片来源不同:主客方 `homeFlag` / `awayFlag` 是接口返回链接,不使用 `v-smart-img`;平局 `drawSign` 是 OSS 素材,单独使用 `v-smart-img`。
|
||||
- Ended 列表用户押注过的选项都使用选中态样式;胜方或平局结果根据接口 `result` 字段判断,`1` 主胜、`2` 平局、`3` 客胜,并在对应押注项图片容器内叠加 `frameWinner`,该图片绝对定位贴顶、宽度 `100%`、最高层展示。
|
||||
- 押注项金额信息分为上下两行,中间使用 `background: #E9D089` 分割线;上方展示选项奖池 `homePoolFormat` / `drawPoolFormat` / `awayPoolFormat`,下方展示用户累计下注额 `myHomeBetFormat` / `myDrawBetFormat` / `myAwayBetFormat`,空值展示 `0`。
|
||||
- 首页 `Ended` 列表和详情页押注对象虽然字段接近,但样式按各自当前页面实现为准,后续只按需求调整字段或内容,不直接合并样式。
|
||||
|
||||
|
||||
@ -365,7 +365,7 @@ export default defineConfig(({ mode }) => {
|
||||
cors: true,
|
||||
proxy: {
|
||||
'/oss': {
|
||||
target: 'https://cdn.azizichat.com',
|
||||
target: 'https://asset.global-interaction.com',
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
rewrite: (path) => path.replace(/^\/oss/, ''),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user