添加赔率

This commit is contained in:
hajimi
2026-07-11 11:19:43 +08:00
parent c9ac46faa7
commit 3a5d571e53
29 changed files with 3006 additions and 542 deletions
+22
View File
@@ -125,6 +125,9 @@ export interface WorldCupOddsMatchItem {
[key: string]: any
}
export type MatchLiveCardItem = WorldCupLiveCardItem
export type MatchOddsMatchItem = WorldCupOddsMatchItem
export function getMatchList(data?: Record<string, any>) {
return request.get({ url: '/match/lists', data: data }, { withToken: false })
}
@@ -165,6 +168,25 @@ export function getWorldCupLiveCards() {
return request.get({ url: '/match/worldCupLiveCards' }, { withToken: false })
}
export function getMatchLiveCards(data?: {
sport_type?: number
league_name?: string
}) {
return request.get({ url: '/match/liveCards', data }, { withToken: false })
}
export function getMatchOdds(data?: {
sport_type?: number
league_name?: string
show_type?: 'live' | 'today' | 'early'
source_site?: string
keyword?: string
include_special?: number
limit?: number
}) {
return request.get({ url: '/match/odds', data }, { withToken: false })
}
export function getWorldCupOdds(data?: {
show_type?: 'live' | 'today' | 'early'
source_site?: string
@@ -5,6 +5,7 @@
<text class="match-header__title-text">赛事中心</text>
<AiAssistantEntry size="compact" />
</view>
<view class="topic-entry" @tap="openWorldCupCenter">
<view class="topic-entry__badge">
<text class="topic-entry__badge-year">2026</text>
@@ -18,146 +19,24 @@
<u-icon name="arrow-right" size="26" color="#ffffff"></u-icon>
</view>
</view>
<view class="topic-tabs">
<view v-for="tab in worldCupQuickTabs" :key="tab.key" class="topic-tabs__item"
@tap="openWorldCupCenter(tab.key)">
<text>{{ tab.label }}</text>
</view>
</view>
<scroll-view scroll-x class="match-header__tabs">
<view class="match-header__tabs-inner">
<view v-for="(tab, idx) in sportTabs" :key="idx" class="match-tab"
:class="{ 'match-tab--active': currentSport === tab.value }" @tap="switchSport(tab.value)">
<text class="match-tab__icon">{{ tab.icon }}</text>
<text class="match-tab__label">{{ tab.label }}</text>
</view>
</view>
</scroll-view>
<scroll-view v-if="leagueTabs.length > 0" scroll-x class="match-header__leagues">
<view class="match-header__leagues-inner">
<view class="league-tab" :class="{ 'league-tab--active': currentLeague === '' }"
@tap="switchLeague('')">
<text>全部<text v-if="leagueTotalCount" class="league-tab__count">·{{ leagueTotalCount
}}</text></text>
</view>
<view v-for="(lg, idx) in leagueTabs" :key="idx" class="league-tab"
:class="{ 'league-tab--active': currentLeague === lg.league_name }"
@tap="switchLeague(lg.league_name)">
<text>{{ lg.league_name }}<text v-if="lg.count" class="league-tab__count">·{{ lg.count
}}</text></text>
</view>
</view>
</scroll-view>
</view>
<scroll-view scroll-y class="match-list" @scrolltolower="loadMore">
<view
v-for="group in matchGroups"
:key="group.title"
class="match-group"
:class="{ 'match-group--ended': group.type === 'ended' }"
>
<view class="match-group__header" @tap="toggleGroup(group.type)">
<view class="match-group__dot" :class="'match-group__dot--' + group.type"></view>
<text class="match-group__title">{{ group.title }}</text>
<text class="match-group__count">{{ group.items.length }}</text>
<view class="match-group__arrow"
:class="{ 'match-group__arrow--collapsed': collapsedGroups[group.type] }">
<text></text>
</view>
</view>
<view v-for="item in group.items" :key="item.id || item.match_id" class="match-card-wrap"
v-show="!collapsedGroups[group.type]" @tap="goDetail(item)">
<match-card :match="item" :show-live-ai="group.type === 'live' || group.type === 'upcoming'" />
</view>
</view>
<view class="match-loading" v-if="loading">
<u-loading mode="circle" />
<text>加载中...</text>
</view>
<view class="match-empty" v-if="!loading && !liveList.length && !upcomingList.length && !endedList.length">
<text>暂无赛事</text>
</view>
<view class="match-nomore" v-if="endedNoMore && endedList.length">
<text>没有更多赛事了</text>
</view>
</scroll-view>
<MatchCenterTabsPanel />
</view>
</template>
<script lang="ts" setup>
import { computed, ref, onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import AiAssistantEntry from '@/components/ai-assistant-entry/ai-assistant-entry.vue'
import MatchCard from '@/components/match-card/match-card.vue'
import { getMatchList, getMatchLeagues, getSportTypes } from '@/api/match'
import { getArticleCate } from '@/api/news'
type WorldCupQuickTab = 'schedule' | 'live' | 'odds'
import MatchCenterTabsPanel from './MatchCenterTabsPanel.vue'
const emit = defineEmits<{
(e: 'open-worldcup', payload: { tab: WorldCupQuickTab; cid?: number | null }): void
(e: 'open-worldcup', payload: { tab: 'schedule'; cid?: number | null }): void
}>()
const statusBarHeight = ref(0)
const sportTabs = ref<any[]>([])
const worldCupCateId = ref<number | null>(null)
const worldCupQuickTabs: { key: WorldCupQuickTab; label: string }[] = [
{ key: 'schedule', label: '赛程' },
{ key: 'live', label: '直播' },
{ key: 'odds', label: '赔率' }
]
const currentSport = ref(0)
const currentLeague = ref('')
const allLeagues = ref<Record<number, any[]>>({})
const liveList = ref<any[]>([])
const upcomingList = ref<any[]>([])
const endedList = ref<any[]>([])
const endedPage = ref(1)
const endedPageSize = 20
const endedTotal = ref(0)
const endedNoMore = ref(false)
const loading = ref(false)
const collapsedGroups = ref<Record<string, boolean>>({
live: false,
upcoming: false,
postponed: false,
ended: false,
other: false
})
const leagueTabs = computed(() => {
if (currentSport.value === 0) {
const merged: any[] = []
Object.keys(allLeagues.value || {}).forEach((key) => {
const items = (allLeagues.value as any)[key]
if (Array.isArray(items)) {
merged.push(...items)
}
})
return merged
}
return allLeagues.value[currentSport.value] || []
})
const leagueTotalCount = computed(() => {
return leagueTabs.value.reduce((sum: number, lg: any) => sum + (lg.count || 0), 0)
})
const matchGroups = computed(() => {
const groups: { title: string; type: string; items: any[]; order: number }[] = []
if (liveList.value.length > 0) {
groups.push({ title: '进行中', type: 'live', items: liveList.value, order: 0 })
}
if (upcomingList.value.length > 0) {
groups.push({ title: '未开始', type: 'upcoming', items: upcomingList.value, order: 1 })
}
if (endedList.value.length > 0) {
groups.push({ title: '已结束', type: 'ended', items: endedList.value, order: 2 })
}
return groups
})
const initLayout = () => {
uni.getSystemInfo({
@@ -168,117 +47,33 @@ const initLayout = () => {
// #ifdef H5
statusBarHeight.value = 0
// #endif
}
},
})
}
const fetchSportTypes = async () => {
try {
const data = await getSportTypes()
sportTabs.value = data || []
} catch (e) {
console.error('获取运动类型失败', e)
}
}
const fetchLeagues = async () => {
try {
const data = await getMatchLeagues()
allLeagues.value = data || {}
} catch (e) {
console.error('获取联赛列表失败', e)
}
}
const fetchWorldCupCateId = async () => {
if (worldCupCateId.value) return worldCupCateId.value
try {
const cateList = await getArticleCate()
const cate = (cateList || []).find((item: any) => {
const category = (cateList || []).find((item: any) => {
const name = item?.name || item?.label || item?.title
return String(name) === '世界杯'
})
worldCupCateId.value = cate?.id || null
} catch (e) {
console.log('获取世界杯分类失败=>', e)
worldCupCateId.value = category?.id || null
} catch (error) {
console.log('获取世界杯分类失败=>', error)
worldCupCateId.value = null
}
return worldCupCateId.value
}
const fetchMatchList = async (refresh = false) => {
if (loading.value) return
if (refresh) {
endedPage.value = 1
endedNoMore.value = false
liveList.value = []
upcomingList.value = []
endedList.value = []
endedTotal.value = 0
}
loading.value = true
try {
const params: Record<string, any> = { ended_page: endedPage.value, ended_page_size: endedPageSize }
if (currentSport.value > 0) params.sport_type = currentSport.value
if (currentLeague.value) params.league_name = currentLeague.value
if (!refresh) params.ended_only = 1
const data = await getMatchList(params)
if (refresh) {
liveList.value = data?.live || []
upcomingList.value = data?.upcoming || []
}
const newEnded = data?.ended?.lists || []
if (refresh) {
endedList.value = newEnded
} else {
endedList.value = endedList.value.concat(newEnded)
}
endedTotal.value = data?.ended?.count || 0
if (newEnded.length < endedPageSize) endedNoMore.value = true
} catch (e) {
console.error('获取赛事失败', e)
} finally {
loading.value = false
}
}
const toggleGroup = (type: string) => {
collapsedGroups.value[type] = !collapsedGroups.value[type]
}
const loadMore = () => {
if (endedNoMore.value || loading.value) return
endedPage.value++
fetchMatchList()
}
const switchSport = (val: number) => {
currentSport.value = val
currentLeague.value = ''
fetchMatchList(true)
}
const switchLeague = (name: string) => {
currentLeague.value = name
fetchMatchList(true)
}
const goDetail = (item: any) => {
const id = item?.id || item?.match_id
if (!id) return
uni.navigateTo({ url: `/pages/match_detail/match_detail?id=${id}` })
}
const openWorldCupCenter = async (tab: WorldCupQuickTab = 'schedule') => {
const openWorldCupCenter = async () => {
const cid = await fetchWorldCupCateId()
emit('open-worldcup', { tab, cid })
emit('open-worldcup', { tab: 'schedule', cid })
}
onMounted(() => {
initLayout()
fetchSportTypes()
fetchLeagues()
fetchMatchList(true)
})
</script>
@@ -302,40 +97,12 @@ onMounted(() => {
align-items: center;
justify-content: space-between;
gap: 16rpx;
&-text {
font-size: 36rpx;
font-weight: 700;
color: #222;
}
}
&__tabs {
white-space: nowrap;
height: 80rpx;
border-bottom: 1rpx solid #f0f0f0;
}
&__tabs-inner {
display: inline-flex;
align-items: center;
height: 60rpx;
padding: 0 12rpx;
gap: 4rpx;
}
&__leagues {
white-space: nowrap;
background: #fff;
}
&__leagues-inner {
display: inline-flex;
flex-wrap: nowrap;
align-items: center;
padding: 8rpx 16rpx 0;
gap: 4rpx;
white-space: nowrap;
&__title-text {
font-size: 36rpx;
font-weight: 700;
color: #222;
}
}
@@ -406,185 +173,4 @@ onMounted(() => {
flex-shrink: 0;
}
}
.topic-tabs {
margin: 0 24rpx 16rpx;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12rpx;
&__item {
height: 58rpx;
border-radius: 16rpx;
background: #f1f5ff;
color: #185dff;
font-size: 26rpx;
font-weight: 700;
line-height: 58rpx;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.league-tab {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8rpx 18rpx;
border-radius: 18rpx;
font-size: 24rpx;
color: #808080;
background: #fff;
white-space: nowrap;
flex-shrink: 0;
&--active {
background: #185dff;
color: #fff;
}
&__count {
font-size: 24rpx;
}
}
.match-tab {
display: inline-flex;
align-items: center;
gap: 0rpx;
padding: 6rpx 16rpx;
border-radius: 18rpx;
background: #f4f5f7;
white-space: nowrap;
flex-shrink: 0;
&__icon {
font-size: 32rpx;
line-height: 1;
}
&__label {
font-size: 28rpx;
color: #666;
line-height: 1.4;
white-space: nowrap;
}
&--active {
background: #e5edff;
.match-tab__label {
color: #185dff;
font-weight: 500;
}
}
}
.match-list {
flex: 1;
height: 0;
min-height: 0;
display: block;
background: #fff;
}
.match-group {
margin-bottom: 2rpx;
&__header {
display: flex;
align-items: center;
padding: 18rpx 18rpx 8rpx;
gap: 6rpx;
background: #fff;
}
&__dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
&--live {
background: #10b981;
}
&--upcoming {
background: #185dff;
}
&--postponed {
background: #ff9900;
}
&--ended {
background: #999;
}
}
&__title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
&__count {
font-size: 20rpx;
color: #999;
}
&__arrow {
margin-left: auto;
font-size: 20rpx;
color: #999;
transition: transform 0.25s ease;
&--collapsed {
transform: rotate(-90deg);
}
}
&--ended {
background: #f3f4f6;
.match-group__header {
background: #f3f4f6;
}
}
}
.match-group:first-child {
margin-top: 0;
}
.match-card-wrap {
position: relative;
margin: 0 18rpx 8rpx;
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: -4rpx;
height: 1rpx;
}
&:last-child::after {
display: none;
}
}
.match-loading,
.match-empty,
.match-nomore {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
gap: 8rpx;
font-size: 24rpx;
color: #999;
}
</style>
File diff suppressed because it is too large Load Diff