feat: add match favorites
This commit is contained in:
@@ -1,15 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="detail-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
<view class="detail-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
<view class="detail-header__nav">
|
<view class="detail-header__nav">
|
||||||
<view class="detail-header__back" @tap="$emit('back')">
|
<view class="detail-header__left">
|
||||||
<u-icon name="arrow-left" size="40" color="#333"></u-icon>
|
<view class="detail-header__back" @tap="$emit('back')">
|
||||||
|
<u-icon name="arrow-left" size="40" color="#333"></u-icon>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-header__title-wrap">
|
<view class="detail-header__title-wrap">
|
||||||
<text class="detail-header__title">{{ title }}</text>
|
<text class="detail-header__title">{{ title }}</text>
|
||||||
<text v-if="subtitle" class="detail-header__subtitle">{{ subtitle }}</text>
|
<text v-if="subtitle" class="detail-header__subtitle">{{ subtitle }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-header__share" @tap="$emit('share')">
|
<view class="detail-header__actions">
|
||||||
<u-icon name="share" size="36" color="#333"></u-icon>
|
<view class="detail-header__favorite" @tap="$emit('favorite')">
|
||||||
|
<u-icon :name="favorite ? 'star-fill' : 'star'" size="34"
|
||||||
|
:color="favorite ? '#e6a23c' : '#333'"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="detail-header__share" @tap="$emit('share')">
|
||||||
|
<u-icon name="share" size="36" color="#333"></u-icon>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -20,10 +28,12 @@ defineProps<{
|
|||||||
title?: string
|
title?: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
statusBarHeight?: number
|
statusBarHeight?: number
|
||||||
|
favorite?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: 'back'): void
|
(e: 'back'): void
|
||||||
|
(e: 'favorite'): void
|
||||||
(e: 'share'): void
|
(e: 'share'): void
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
@@ -40,12 +50,14 @@ defineEmits<{
|
|||||||
&__nav {
|
&__nav {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 144rpx minmax(0, 1fr) 144rpx;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 96rpx;
|
height: 96rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__back,
|
&__back,
|
||||||
|
&__favorite,
|
||||||
&__share {
|
&__share {
|
||||||
width: 72rpx;
|
width: 72rpx;
|
||||||
height: 72rpx;
|
height: 72rpx;
|
||||||
@@ -56,8 +68,18 @@ defineEmits<{
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__left,
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
&__title-wrap {
|
&__title-wrap {
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<u-icon :name="tab.icon" size="28" :color="activeType === tab.key ? '#1468f5' : '#7b8492'" />
|
<u-icon :name="tab.icon" size="28" :color="activeType === tab.key ? '#1468f5' : '#7b8492'" />
|
||||||
<text>{{ tab.label }}</text>
|
<text>{{ tab.label }}</text>
|
||||||
<text v-if="tab.key === 'article' && articleCount" class="collection-tab__count">{{ articleCount }}</text>
|
<text v-if="tab.key === 'article' && articleCount" class="collection-tab__count">{{ articleCount }}</text>
|
||||||
|
<text v-if="tab.key === 'match' && matchCount" class="collection-tab__count">{{ matchCount }}</text>
|
||||||
<text v-if="tab.key === 'lottery' && lotteryCount" class="collection-tab__count">{{ lotteryCount }}</text>
|
<text v-if="tab.key === 'lottery' && lotteryCount" class="collection-tab__count">{{ lotteryCount }}</text>
|
||||||
<text v-if="tab.key === 'crypto' && cryptoCount" class="collection-tab__count">{{ cryptoCount }}</text>
|
<text v-if="tab.key === 'crypto' && cryptoCount" class="collection-tab__count">{{ cryptoCount }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -76,6 +77,45 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="activeType === 'match'">
|
||||||
|
<view v-if="matchFavorites.length" class="collection-list match-list">
|
||||||
|
<view v-for="item in matchFavorites" :key="item.id" class="match-item" @tap="openMatch(item)">
|
||||||
|
<view class="match-item__top">
|
||||||
|
<text class="match-item__league">{{ item.league_name || '赛事' }}{{ item.round_name ? ` · ${item.round_name}` : '' }}</text>
|
||||||
|
<text class="match-item__status">{{ getMatchFavoriteStatus(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="match-item__content">
|
||||||
|
<view class="match-item__team">
|
||||||
|
<image v-if="item.home_icon" :src="item.home_icon" mode="aspectFit" />
|
||||||
|
<view v-else class="match-item__team-logo">{{ item.home_team.slice(0, 1) || '?' }}</view>
|
||||||
|
<text>{{ item.home_team || '主队' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="match-item__center">
|
||||||
|
<view v-if="item.status !== 0" class="match-item__score">
|
||||||
|
<text>{{ item.home_score ?? 0 }}</text>
|
||||||
|
<text>:</text>
|
||||||
|
<text>{{ item.away_score ?? 0 }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-else class="match-item__time">{{ formatMatchFavoriteTime(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="match-item__team">
|
||||||
|
<image v-if="item.away_icon" :src="item.away_icon" mode="aspectFit" />
|
||||||
|
<view v-else class="match-item__team-logo">{{ item.away_team.slice(0, 1) || '?' }}</view>
|
||||||
|
<text>{{ item.away_team || '客队' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="match-item__remove" @tap.stop="removeMatch(item)">
|
||||||
|
<u-icon name="star-fill" size="28" color="#e6a23c" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="collection-state collection-state--empty">
|
||||||
|
<u-icon name="calendar" size="72" color="#cbd5e1" />
|
||||||
|
<text class="collection-state__title">暂无比赛收藏</text>
|
||||||
|
<text class="collection-state__desc">在赛事详情页点击星标即可加入收藏</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeType === 'lottery'">
|
<template v-else-if="activeType === 'lottery'">
|
||||||
<view v-if="lotteryFavorites.length" class="collection-list lottery-list">
|
<view v-if="lotteryFavorites.length" class="collection-list lottery-list">
|
||||||
<view v-for="item in lotteryFavorites" :key="item.code" class="lottery-item"
|
<view v-for="item in lotteryFavorites" :key="item.code" class="lottery-item"
|
||||||
@@ -155,6 +195,8 @@ import {
|
|||||||
type CryptoMarketCoin
|
type CryptoMarketCoin
|
||||||
} from '@/api/crypto'
|
} from '@/api/crypto'
|
||||||
import { getLotteryFavoriteCodes, setLotteryFavoriteCodes } from '@/utils/lottery-favorite'
|
import { getLotteryFavoriteCodes, setLotteryFavoriteCodes } from '@/utils/lottery-favorite'
|
||||||
|
import { getMatchFavorites, setMatchFavorites, type MatchFavoriteItem } from '@/utils/match-favorite'
|
||||||
|
import { getLocalMatchDate } from '@/utils/match-time'
|
||||||
|
|
||||||
type CollectionType = 'article' | 'post' | 'match' | 'lottery' | 'crypto'
|
type CollectionType = 'article' | 'post' | 'match' | 'lottery' | 'crypto'
|
||||||
|
|
||||||
@@ -168,15 +210,17 @@ const collectionTabs: Array<{ key: CollectionType; label: string; icon: string }
|
|||||||
|
|
||||||
const activeType = ref<CollectionType>('article')
|
const activeType = ref<CollectionType>('article')
|
||||||
const articles = ref<any[]>([])
|
const articles = ref<any[]>([])
|
||||||
|
const matchFavorites = ref<MatchFavoriteItem[]>(getMatchFavorites())
|
||||||
const lotteryFavorites = ref<any[]>(getLotteryFavoriteCodes().map((code) => ({ code })))
|
const lotteryFavorites = ref<any[]>(getLotteryFavoriteCodes().map((code) => ({ code })))
|
||||||
const coins = ref<CryptoMarketCoin[]>([])
|
const coins = ref<CryptoMarketCoin[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
|
|
||||||
const articleCount = computed(() => articles.value.length)
|
const articleCount = computed(() => articles.value.length)
|
||||||
|
const matchCount = computed(() => matchFavorites.value.length)
|
||||||
const lotteryCount = computed(() => lotteryFavorites.value.length)
|
const lotteryCount = computed(() => lotteryFavorites.value.length)
|
||||||
const cryptoCount = computed(() => coins.value.length)
|
const cryptoCount = computed(() => coins.value.length)
|
||||||
const collectionCount = computed(() => articleCount.value + lotteryCount.value + cryptoCount.value)
|
const collectionCount = computed(() => articleCount.value + matchCount.value + lotteryCount.value + cryptoCount.value)
|
||||||
const activeTabLabel = computed(() => collectionTabs.find((tab) => tab.key === activeType.value)?.label || '')
|
const activeTabLabel = computed(() => collectionTabs.find((tab) => tab.key === activeType.value)?.label || '')
|
||||||
|
|
||||||
const loadArticles = async () => {
|
const loadArticles = async () => {
|
||||||
@@ -195,6 +239,10 @@ const loadCrypto = async () => {
|
|||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadMatches = () => {
|
||||||
|
matchFavorites.value = getMatchFavorites()
|
||||||
|
}
|
||||||
|
|
||||||
const loadLottery = async () => {
|
const loadLottery = async () => {
|
||||||
const favoriteCodes = getLotteryFavoriteCodes()
|
const favoriteCodes = getLotteryFavoriteCodes()
|
||||||
if (!favoriteCodes.length) {
|
if (!favoriteCodes.length) {
|
||||||
@@ -234,6 +282,8 @@ const loadCollection = async () => {
|
|||||||
try {
|
try {
|
||||||
if (activeType.value === 'article') {
|
if (activeType.value === 'article') {
|
||||||
await loadArticles()
|
await loadArticles()
|
||||||
|
} else if (activeType.value === 'match') {
|
||||||
|
loadMatches()
|
||||||
} else if (activeType.value === 'lottery') {
|
} else if (activeType.value === 'lottery') {
|
||||||
await loadLottery()
|
await loadLottery()
|
||||||
} else if (activeType.value === 'crypto') {
|
} else if (activeType.value === 'crypto') {
|
||||||
@@ -276,6 +326,31 @@ const removeArticle = async (item: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openMatch = (item: MatchFavoriteItem) => {
|
||||||
|
uni.navigateTo({ url: `/pages/match_detail/match_detail?id=${item.id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMatch = (item: MatchFavoriteItem) => {
|
||||||
|
matchFavorites.value = setMatchFavorites(matchFavorites.value.filter((favorite) => favorite.id !== item.id))
|
||||||
|
uni.showToast({ title: '已取消收藏', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMatchFavoriteStatus = (item: MatchFavoriteItem) => {
|
||||||
|
if (item.status === 1) return item.current_minute || '直播中'
|
||||||
|
if (item.status === 0) return '未开始'
|
||||||
|
return '已结束'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMatchFavoriteTime = (item: MatchFavoriteItem) => {
|
||||||
|
const date = getLocalMatchDate(item.match_time, item)
|
||||||
|
if (!date) return '时间待定'
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${month}-${day} ${hour}:${minute}`
|
||||||
|
}
|
||||||
|
|
||||||
const openCoin = (coin: CryptoMarketCoin) => {
|
const openCoin = (coin: CryptoMarketCoin) => {
|
||||||
uni.navigateTo({ url: `/pages/crypto/crypto_detail?symbol=${encodeURIComponent(coin.symbol)}` })
|
uni.navigateTo({ url: `/pages/crypto/crypto_detail?symbol=${encodeURIComponent(coin.symbol)}` })
|
||||||
}
|
}
|
||||||
@@ -320,7 +395,7 @@ const formatChange = (value: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
if (activeType.value === 'article' || activeType.value === 'lottery' || activeType.value === 'crypto') {
|
if (activeType.value === 'article' || activeType.value === 'match' || activeType.value === 'lottery' || activeType.value === 'crypto') {
|
||||||
loadCollection()
|
loadCollection()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -600,6 +675,112 @@ onShow(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.match-item {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
background: #fff;
|
||||||
|
border: 1rpx solid #e7eaf0;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
box-shadow: 0 8rpx 30rpx rgba(29, 45, 78, 0.04);
|
||||||
|
animation: collection-rise 0.32s ease both;
|
||||||
|
|
||||||
|
&__top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__league {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #235bde;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #7d8797;
|
||||||
|
font-size: 21rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 130rpx minmax(0, 1fr) 42rpx;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__team {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
color: #202633;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
image,
|
||||||
|
&-logo {
|
||||||
|
width: 46rpx;
|
||||||
|
height: 46rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
image {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__team-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #edf3ff;
|
||||||
|
color: #1468f5;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__center {
|
||||||
|
min-width: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__score {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7rpx;
|
||||||
|
color: #151b26;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
display: block;
|
||||||
|
color: #7d8797;
|
||||||
|
font-size: 19rpx;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__remove {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.lottery-item {
|
.lottery-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="match-detail">
|
<view class="match-detail">
|
||||||
<match-detail-header :status-bar-height="statusBarHeight" :title="matchDetailTitle" @back="goBack"
|
<match-detail-header :status-bar-height="statusBarHeight" :title="matchDetailTitle" @back="goBack"
|
||||||
@share="showSharePopup = true" />
|
:favorite="isFavorite" @favorite="toggleFavorite" @share="showSharePopup = true" />
|
||||||
|
|
||||||
<!-- 加载中 -->
|
<!-- 加载中 -->
|
||||||
<view class="detail-loading" v-if="loading">
|
<view class="detail-loading" v-if="loading">
|
||||||
@@ -463,13 +463,14 @@ import MatchDetailHeader from '@/components/match-detail-header/match-detail-hea
|
|||||||
import MatchOddsDetail from '@/components/match-odds-detail/match-odds-detail.vue'
|
import MatchOddsDetail from '@/components/match-odds-detail/match-odds-detail.vue'
|
||||||
import SharePopup from '@/components/share-popup/share-popup.vue'
|
import SharePopup from '@/components/share-popup/share-popup.vue'
|
||||||
import GlobalPopup from '@/components/global-popup/global-popup.vue'
|
import GlobalPopup from '@/components/global-popup/global-popup.vue'
|
||||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
|
||||||
import { getMatchDetail, getMatchLiveText, getMatchLineup } from '@/api/match'
|
import { getMatchDetail, getMatchLiveText, getMatchLineup } from '@/api/match'
|
||||||
import type { MatchLiveLineItem } from '@/api/match'
|
import type { MatchLiveLineItem } from '@/api/match'
|
||||||
import { checkAiUnlock, getMatchPredictSection } from '@/api/ai'
|
import { checkAiUnlock, getMatchPredictSection } from '@/api/ai'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { getLocalMatchDate } from '@/utils/match-time'
|
import { getLocalMatchDate } from '@/utils/match-time'
|
||||||
import { AI_DAILY_FREE_TEXT, getAiFreeRemain } from '@/config/ai'
|
import { AI_DAILY_FREE_TEXT, getAiFreeRemain } from '@/config/ai'
|
||||||
|
import { isMatchFavorite, toggleMatchFavorite } from '@/utils/match-favorite'
|
||||||
|
|
||||||
type MatchDetailLiveLine = MatchLiveLineItem & {
|
type MatchDetailLiveLine = MatchLiveLineItem & {
|
||||||
lineKey: string
|
lineKey: string
|
||||||
@@ -494,6 +495,7 @@ const aiUnlocked = ref(false)
|
|||||||
const aiInlineLoading = ref(false)
|
const aiInlineLoading = ref(false)
|
||||||
const aiInline = ref<any>({})
|
const aiInline = ref<any>({})
|
||||||
const showSharePopup = ref(false)
|
const showSharePopup = ref(false)
|
||||||
|
const isFavorite = ref(false)
|
||||||
const liveTextList = ref<any[]>([])
|
const liveTextList = ref<any[]>([])
|
||||||
const activeLiveLineKey = ref('')
|
const activeLiveLineKey = ref('')
|
||||||
const activeLiveError = ref('')
|
const activeLiveError = ref('')
|
||||||
@@ -741,6 +743,7 @@ const fetchDetail = async (id: number) => {
|
|||||||
try {
|
try {
|
||||||
const data = await getMatchDetail({ id })
|
const data = await getMatchDetail({ id })
|
||||||
match.value = data || {}
|
match.value = data || {}
|
||||||
|
isFavorite.value = isMatchFavorite(match.value?.id || id)
|
||||||
if (data?.live_text?.length) {
|
if (data?.live_text?.length) {
|
||||||
liveTextList.value = data.live_text
|
liveTextList.value = data.live_text
|
||||||
}
|
}
|
||||||
@@ -883,6 +886,13 @@ const goBack = () => {
|
|||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleFavorite = () => {
|
||||||
|
if (!match.value?.id) return
|
||||||
|
const result = toggleMatchFavorite(match.value)
|
||||||
|
isFavorite.value = result.favorite
|
||||||
|
uni.showToast({ title: result.favorite ? '已收藏比赛' : '已取消收藏', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
const goAiAnalysis = () => {
|
const goAiAnalysis = () => {
|
||||||
const id = match.value?.id
|
const id = match.value?.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@@ -973,6 +983,10 @@ onLoad((options) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (match.value?.id) isFavorite.value = isMatchFavorite(match.value.id)
|
||||||
|
})
|
||||||
|
|
||||||
onUnload(() => {
|
onUnload(() => {
|
||||||
stopLiveTextPolling()
|
stopLiveTextPolling()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
const MATCH_FAVORITE_STORAGE_KEY = 'match_favorites'
|
||||||
|
|
||||||
|
export interface MatchFavoriteItem {
|
||||||
|
id: number
|
||||||
|
league_name: string
|
||||||
|
round_name: string
|
||||||
|
home_team: string
|
||||||
|
home_icon: string
|
||||||
|
home_score: number | string
|
||||||
|
away_team: string
|
||||||
|
away_icon: string
|
||||||
|
away_score: number | string
|
||||||
|
match_time: number | string
|
||||||
|
status: number
|
||||||
|
current_minute: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeFavorite = (value: any): MatchFavoriteItem | null => {
|
||||||
|
const id = Number(value?.id)
|
||||||
|
if (!Number.isFinite(id) || id <= 0) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
league_name: String(value?.league_name || ''),
|
||||||
|
round_name: String(value?.round_name || ''),
|
||||||
|
home_team: String(value?.home_team || ''),
|
||||||
|
home_icon: String(value?.home_icon || ''),
|
||||||
|
home_score: value?.home_score ?? 0,
|
||||||
|
away_team: String(value?.away_team || ''),
|
||||||
|
away_icon: String(value?.away_icon || ''),
|
||||||
|
away_score: value?.away_score ?? 0,
|
||||||
|
match_time: value?.match_time ?? '',
|
||||||
|
status: Number(value?.status || 0),
|
||||||
|
current_minute: String(value?.current_minute || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMatchFavorites = (): MatchFavoriteItem[] => {
|
||||||
|
const stored = uni.getStorageSync(MATCH_FAVORITE_STORAGE_KEY)
|
||||||
|
if (!Array.isArray(stored)) return []
|
||||||
|
|
||||||
|
const seen = new Set<number>()
|
||||||
|
return stored.reduce<MatchFavoriteItem[]>((favorites, item) => {
|
||||||
|
const favorite = normalizeFavorite(item)
|
||||||
|
if (!favorite || seen.has(favorite.id)) return favorites
|
||||||
|
seen.add(favorite.id)
|
||||||
|
favorites.push(favorite)
|
||||||
|
return favorites
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setMatchFavorites = (favorites: MatchFavoriteItem[]): MatchFavoriteItem[] => {
|
||||||
|
const seen = new Set<number>()
|
||||||
|
const normalized = favorites.reduce<MatchFavoriteItem[]>((items, favorite) => {
|
||||||
|
const normalizedFavorite = normalizeFavorite(favorite)
|
||||||
|
if (!normalizedFavorite || seen.has(normalizedFavorite.id)) return items
|
||||||
|
seen.add(normalizedFavorite.id)
|
||||||
|
items.push(normalizedFavorite)
|
||||||
|
return items
|
||||||
|
}, [])
|
||||||
|
uni.setStorageSync(MATCH_FAVORITE_STORAGE_KEY, normalized)
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMatchFavorite = (matchId: number | string): boolean => {
|
||||||
|
const id = Number(matchId)
|
||||||
|
return getMatchFavorites().some((favorite) => favorite.id === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleMatchFavorite = (match: any): { favorite: boolean; favorites: MatchFavoriteItem[] } => {
|
||||||
|
const item = normalizeFavorite(match)
|
||||||
|
const favorites = getMatchFavorites()
|
||||||
|
if (!item) return { favorite: false, favorites }
|
||||||
|
|
||||||
|
const favorite = favorites.some((existing) => existing.id === item.id)
|
||||||
|
const nextFavorites = favorite
|
||||||
|
? favorites.filter((existing) => existing.id !== item.id)
|
||||||
|
: [item, ...favorites]
|
||||||
|
|
||||||
|
return { favorite: !favorite, favorites: setMatchFavorites(nextFavorites) }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user