233 lines
6.8 KiB
TypeScript
233 lines
6.8 KiB
TypeScript
import { ref, computed } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import api from '../api';
|
|
import type { BannerItem } from '../components/BannerCarousel.vue';
|
|
import { resolveBanners } from '../constants/defaultBanner';
|
|
import { resolveAnnouncements } from '../constants/defaultAnnouncement';
|
|
import { stripHtml } from '../utils/html';
|
|
|
|
export interface PlayerHomeMatchSelection {
|
|
id: string;
|
|
selectionCode?: string;
|
|
selectionName: string;
|
|
selectionDisplayName?: string;
|
|
odds: string;
|
|
status?: string;
|
|
oddsVersion?: string;
|
|
}
|
|
|
|
export interface PlayerHomeMatchMarket {
|
|
id: string;
|
|
marketType: string;
|
|
marketDisplayName?: string;
|
|
lineValue?: number | null;
|
|
status: string;
|
|
allowSingle?: boolean;
|
|
allowParlay?: boolean;
|
|
selections: PlayerHomeMatchSelection[];
|
|
}
|
|
|
|
export interface PlayerHomeMatch {
|
|
id: string;
|
|
leagueName?: string;
|
|
leagueLogoUrl?: string | null;
|
|
homeTeamName: string;
|
|
awayTeamName: string;
|
|
homeTeamCode?: string;
|
|
awayTeamCode?: string;
|
|
homeTeamLogoUrl?: string | null;
|
|
awayTeamLogoUrl?: string | null;
|
|
startTime: string;
|
|
isHot?: boolean;
|
|
displayOrder?: number;
|
|
bettingOpen?: boolean;
|
|
markets?: PlayerHomeMatchMarket[];
|
|
}
|
|
|
|
export interface PlayerContentItem {
|
|
id: string;
|
|
contentType?: string;
|
|
sortOrder?: number;
|
|
createdAt?: string;
|
|
linkType?: string | null;
|
|
linkTarget?: string | null;
|
|
translation?: {
|
|
title?: string | null;
|
|
body?: string | null;
|
|
imageUrl?: string | null;
|
|
};
|
|
}
|
|
|
|
export type PlayerAnnouncementItem = PlayerContentItem;
|
|
|
|
interface HomePayload {
|
|
banners?: PlayerContentItem[];
|
|
announcements?: PlayerAnnouncementItem[];
|
|
ticker?: PlayerAnnouncementItem[];
|
|
notices?: PlayerAnnouncementItem[];
|
|
hotMatches?: PlayerHomeMatch[];
|
|
upcomingMatches?: PlayerHomeMatch[];
|
|
inboxEnabled?: boolean;
|
|
}
|
|
|
|
function mergeMatchList(
|
|
existing: PlayerHomeMatch[] | undefined,
|
|
fresh: PlayerHomeMatch[] | undefined,
|
|
): PlayerHomeMatch[] | undefined {
|
|
if (!fresh) return existing;
|
|
if (!existing) return fresh;
|
|
|
|
const freshMap = new Map(fresh.map((m) => [m.id, m]));
|
|
for (const m of existing) {
|
|
const f = freshMap.get(m.id);
|
|
if (f) Object.assign(m, f);
|
|
}
|
|
const existingIds = new Set(existing.map((m) => m.id));
|
|
for (const fm of fresh) {
|
|
if (!existingIds.has(fm.id)) existing.push(fm);
|
|
}
|
|
for (let i = existing.length - 1; i >= 0; i--) {
|
|
if (!freshMap.has(existing[i].id)) existing.splice(i, 1);
|
|
}
|
|
return existing;
|
|
}
|
|
|
|
const homeRaw = ref<HomePayload | null>(null);
|
|
const loading = ref(false);
|
|
|
|
function collectAnnouncementLines(data: HomePayload | null): string[] {
|
|
if (!data) return [];
|
|
const source =
|
|
data.announcements && data.announcements.length > 0
|
|
? data.announcements
|
|
: [...(data.ticker ?? []), ...(data.notices ?? [])];
|
|
|
|
const lines: string[] = [];
|
|
for (const item of source) {
|
|
const title = item.translation?.title?.trim();
|
|
const text = title || stripHtml(item.translation?.body ?? '');
|
|
if (text) lines.push(text);
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
function collectAnnouncementItems(data: HomePayload | null): PlayerAnnouncementItem[] {
|
|
if (!data) return [];
|
|
const seen = new Set<string>();
|
|
const merged: PlayerAnnouncementItem[] = [];
|
|
|
|
const pushItems = (items: PlayerAnnouncementItem[] | undefined) => {
|
|
for (const item of items ?? []) {
|
|
if (!item?.id || seen.has(item.id)) continue;
|
|
if (!item.translation?.title && !item.translation?.body) continue;
|
|
seen.add(item.id);
|
|
merged.push(item);
|
|
}
|
|
};
|
|
|
|
pushItems(data.banners);
|
|
pushItems(data.announcements);
|
|
pushItems(data.ticker);
|
|
pushItems(data.notices);
|
|
|
|
merged.sort((a, b) => {
|
|
const timeA = a.createdAt ? Date.parse(a.createdAt) : 0;
|
|
const timeB = b.createdAt ? Date.parse(b.createdAt) : 0;
|
|
if (timeB !== timeA) return timeB - timeA;
|
|
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
|
|
});
|
|
|
|
return merged;
|
|
}
|
|
|
|
function collectHubContentItems(data: HomePayload | null): PlayerContentItem[] {
|
|
if (!data) return [];
|
|
const seen = new Set<string>();
|
|
const merged: PlayerContentItem[] = [];
|
|
|
|
const pushItems = (items: PlayerContentItem[] | undefined, fallbackType?: string) => {
|
|
for (const item of items ?? []) {
|
|
if (!item?.id || seen.has(item.id)) continue;
|
|
const hasContent =
|
|
item.translation?.title?.trim() ||
|
|
item.translation?.body?.trim() ||
|
|
item.translation?.imageUrl?.trim();
|
|
if (!hasContent) continue;
|
|
seen.add(item.id);
|
|
merged.push({
|
|
...item,
|
|
contentType: item.contentType || fallbackType,
|
|
});
|
|
}
|
|
};
|
|
|
|
pushItems(data.banners, 'BANNER');
|
|
pushItems(data.ticker, 'TICKER');
|
|
pushItems(data.notices, 'NOTICE');
|
|
pushItems(data.announcements);
|
|
|
|
merged.sort((a, b) => {
|
|
const timeA = a.createdAt ? Date.parse(a.createdAt) : 0;
|
|
const timeB = b.createdAt ? Date.parse(b.createdAt) : 0;
|
|
if (timeB !== timeA) return timeB - timeA;
|
|
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
|
|
});
|
|
return merged;
|
|
}
|
|
|
|
/** 管理端公共内容 → 玩家端首页/跑马灯(单例,避免重复请求) */
|
|
export function usePlayerHome() {
|
|
const { t } = useI18n();
|
|
|
|
async function load(force = false) {
|
|
if (!force && homeRaw.value) return;
|
|
loading.value = true;
|
|
try {
|
|
const { data } = await api.get('/player/home');
|
|
const fresh = (data.data ?? null) as HomePayload | null;
|
|
|
|
if (fresh && homeRaw.value) {
|
|
// 已有数据 → 原地更新,保留对象引用,避免图片重新加载
|
|
const existing = homeRaw.value;
|
|
existing.banners = fresh.banners;
|
|
existing.announcements = fresh.announcements;
|
|
existing.ticker = fresh.ticker;
|
|
existing.notices = fresh.notices;
|
|
existing.inboxEnabled = fresh.inboxEnabled;
|
|
|
|
existing.hotMatches = mergeMatchList(existing.hotMatches, fresh.hotMatches);
|
|
existing.upcomingMatches = mergeMatchList(existing.upcomingMatches, fresh.upcomingMatches);
|
|
} else {
|
|
homeRaw.value = fresh;
|
|
}
|
|
} catch {
|
|
homeRaw.value = null;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
const banners = computed(() => resolveBanners(homeRaw.value?.banners as BannerItem[] | undefined));
|
|
const bannerItems = computed(() => homeRaw.value?.banners ?? []);
|
|
const announcements = computed(() =>
|
|
resolveAnnouncements(collectAnnouncementLines(homeRaw.value), t('home.announcement_default')),
|
|
);
|
|
const announcementItems = computed(() => collectAnnouncementItems(homeRaw.value));
|
|
const hubContentItems = computed(() => collectHubContentItems(homeRaw.value));
|
|
const hotMatches = computed(() => homeRaw.value?.hotMatches ?? []);
|
|
const upcomingMatches = computed(() => homeRaw.value?.upcomingMatches ?? []);
|
|
|
|
return {
|
|
homeRaw,
|
|
loading,
|
|
load,
|
|
banners,
|
|
bannerItems,
|
|
announcements,
|
|
announcementItems,
|
|
hubContentItems,
|
|
hotMatches,
|
|
upcomingMatches,
|
|
};
|
|
}
|