Files
thebet365/apps/player/src/composables/usePlayerHome.ts
Mars 8a1ba0fd95 feat(player): 新增桌面端 UI 与响应式双端路由架构
- 将原移动端页面重命名为 Mobile*,新增 22 个 Desktop* 视图与 DesktopShell 布局体系
- 路由入口改为 Wrapper 视图,通过 useViewport(≥1024px)自动切换移动/桌面端
- 新增投注单面板、盘口弹层、联赛侧栏、数据表格等桌面组件及独立样式令牌
- 扩展 betSlip store、串关筛选、冠军盘、Toast/悬浮信箱等交互与三语 i18n
- 公告/消息 Hub 拆分移动与桌面实现;API 投注/钱包/充值记录支持 pageSize 分页
2026-06-29 18:01:39 +08:00

190 lines
5.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 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;
}
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 source =
data.announcements && data.announcements.length > 0
? data.announcements
: [...(data.ticker ?? []), ...(data.notices ?? [])];
return source.filter((item) => item.translation?.title || item.translation?.body);
}
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,
};
}