diff --git a/uniapp/src/components/match-detail-header/match-detail-header.vue b/uniapp/src/components/match-detail-header/match-detail-header.vue
index f7806d17..01afddd0 100644
--- a/uniapp/src/components/match-detail-header/match-detail-header.vue
+++ b/uniapp/src/components/match-detail-header/match-detail-header.vue
@@ -1,15 +1,23 @@
+
+
+
+
+ {{ item.league_name || '赛事' }}{{ item.round_name ? ` · ${item.round_name}` : '' }}
+ {{ getMatchFavoriteStatus(item) }}
+
+
+
+
+ {{ item.home_team.slice(0, 1) || '?' }}
+ {{ item.home_team || '主队' }}
+
+
+
+ {{ item.home_score ?? 0 }}
+ :
+ {{ item.away_score ?? 0 }}
+
+ {{ formatMatchFavoriteTime(item) }}
+
+
+
+ {{ item.away_team.slice(0, 1) || '?' }}
+ {{ item.away_team || '客队' }}
+
+
+
+
+
+
+
+
+
+ 暂无比赛收藏
+ 在赛事详情页点击星标即可加入收藏
+
+
+
('article')
const articles = ref([])
+const matchFavorites = ref(getMatchFavorites())
const lotteryFavorites = ref(getLotteryFavoriteCodes().map((code) => ({ code })))
const coins = ref([])
const loading = ref(false)
const refreshing = ref(false)
const articleCount = computed(() => articles.value.length)
+const matchCount = computed(() => matchFavorites.value.length)
const lotteryCount = computed(() => lotteryFavorites.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 loadArticles = async () => {
@@ -195,6 +239,10 @@ const loadCrypto = async () => {
: []
}
+const loadMatches = () => {
+ matchFavorites.value = getMatchFavorites()
+}
+
const loadLottery = async () => {
const favoriteCodes = getLotteryFavoriteCodes()
if (!favoriteCodes.length) {
@@ -234,6 +282,8 @@ const loadCollection = async () => {
try {
if (activeType.value === 'article') {
await loadArticles()
+ } else if (activeType.value === 'match') {
+ loadMatches()
} else if (activeType.value === 'lottery') {
await loadLottery()
} 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) => {
uni.navigateTo({ url: `/pages/crypto/crypto_detail?symbol=${encodeURIComponent(coin.symbol)}` })
}
@@ -320,7 +395,7 @@ const formatChange = (value: number) => {
}
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()
}
})
@@ -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 {
display: flex;
align-items: center;
diff --git a/uniapp/src/pages/match_detail/match_detail.vue b/uniapp/src/pages/match_detail/match_detail.vue
index f557a595..578e7260 100644
--- a/uniapp/src/pages/match_detail/match_detail.vue
+++ b/uniapp/src/pages/match_detail/match_detail.vue
@@ -1,7 +1,7 @@
+ :favorite="isFavorite" @favorite="toggleFavorite" @share="showSharePopup = true" />
@@ -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 SharePopup from '@/components/share-popup/share-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 type { MatchLiveLineItem } from '@/api/match'
import { checkAiUnlock, getMatchPredictSection } from '@/api/ai'
import { useUserStore } from '@/stores/user'
import { getLocalMatchDate } from '@/utils/match-time'
import { AI_DAILY_FREE_TEXT, getAiFreeRemain } from '@/config/ai'
+import { isMatchFavorite, toggleMatchFavorite } from '@/utils/match-favorite'
type MatchDetailLiveLine = MatchLiveLineItem & {
lineKey: string
@@ -494,6 +495,7 @@ const aiUnlocked = ref(false)
const aiInlineLoading = ref(false)
const aiInline = ref({})
const showSharePopup = ref(false)
+const isFavorite = ref(false)
const liveTextList = ref([])
const activeLiveLineKey = ref('')
const activeLiveError = ref('')
@@ -741,6 +743,7 @@ const fetchDetail = async (id: number) => {
try {
const data = await getMatchDetail({ id })
match.value = data || {}
+ isFavorite.value = isMatchFavorite(match.value?.id || id)
if (data?.live_text?.length) {
liveTextList.value = data.live_text
}
@@ -883,6 +886,13 @@ const goBack = () => {
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 id = match.value?.id
if (!id) return
@@ -973,6 +983,10 @@ onLoad((options) => {
}
})
+onShow(() => {
+ if (match.value?.id) isFavorite.value = isMatchFavorite(match.value.id)
+})
+
onUnload(() => {
stopLiveTextPolling()
})
diff --git a/uniapp/src/utils/match-favorite.ts b/uniapp/src/utils/match-favorite.ts
new file mode 100644
index 00000000..a889a85c
--- /dev/null
+++ b/uniapp/src/utils/match-favorite.ts
@@ -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()
+ return stored.reduce((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()
+ const normalized = favorites.reduce((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) }
+}