Files
thebet365/apps/player/src/views/HomeView.vue
Mars 312c3c5816 feat(player): 注册账号、登录双模式与移动端性能优化
注册必填 7-32 位账号,手机号区号/本地号分存;登录默认账号模式并支持切换手机号登录;Player i18n 拆包与赛事接口优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 10:56:51 +08:00

386 lines
7.9 KiB
Vue

<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import emptyMatchesImg from '../assets/images/empty-matches.svg';
import vsImg from '../assets/images/vs.png';
import cardBg from '../assets/images/card-bg.png';
import BannerCarousel from '../components/BannerCarousel.vue';
import { usePlayerHome } from '../composables/usePlayerHome';
import TeamEmblem from '../components/TeamEmblem.vue';
import GoldSpinner from '../components/GoldSpinner.vue';
import { usePullToRefresh } from '../composables/usePullToRefresh';
const matchCardBg = `url(${cardBg})`;
const { t, locale } = useI18n();
const router = useRouter();
const { banners, hotMatches, loading, load } = usePlayerHome();
const { pullDistance, refreshing, spinning, progress } = usePullToRefresh({
onRefresh: async () => { await load(true); },
});
const pullIndicatorStyle = () => ({
height: `${pullDistance.value}px`,
opacity: Math.min(pullDistance.value / 48, 1),
});
function goMatch(id: string) {
router.push(`/match/${id}`);
}
function formatKickoff(startTime: string) {
return new Date(startTime).toLocaleString(locale.value, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
}
</script>
<template>
<div>
<div
class="pull-indicator"
:style="pullIndicatorStyle()"
>
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
</div>
<BannerCarousel :banners="banners" />
<h2 class="section-title">{{ t('home.hot_matches') }}</h2>
<div
v-for="(match, index) in hotMatches"
:key="match.id"
class="match-card"
:class="{ 'match-card--live-anim': index < 3 }"
@click="goMatch(match.id)"
>
<div class="match-info">
<div class="match-teams">{{ match.homeTeamName }} vs {{ match.awayTeamName }}</div>
<div class="match-time">{{ formatKickoff(match.startTime) }}</div>
</div>
<div class="match-flags" aria-hidden="true">
<TeamEmblem
size="md"
:team-code="match.homeTeamCode"
:team-name="match.homeTeamName"
:logo-url="match.homeTeamLogoUrl"
/>
<div class="vs-arena">
<svg class="hz-lightning" viewBox="0 0 72 28" aria-hidden="true">
<defs>
<linearGradient :id="`hzBoltGrad-${match.id}`" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#5eb8ff" stop-opacity="0.2" />
<stop offset="35%" stop-color="#b8ecff" stop-opacity="1" />
<stop offset="50%" stop-color="#ffffff" stop-opacity="1" />
<stop offset="65%" stop-color="#ffd080" stop-opacity="1" />
<stop offset="100%" stop-color="#ff9040" stop-opacity="0.2" />
</linearGradient>
</defs>
<path
class="hz-path hz-path-main"
:stroke="`url(#hzBoltGrad-${match.id})`"
d="M1 14 H16 L20 5 L24 23 L28 9 L32 14 H40 L44 6 L48 22 L52 12 L56 14 H71"
/>
<path
class="hz-path hz-path-sub"
:stroke="`url(#hzBoltGrad-${match.id})`"
d="M3 19 H14 L18 15 L22 19 H50 L54 16 L58 19 H69"
/>
</svg>
<span class="hz-beam" aria-hidden="true" />
<img :src="vsImg" alt="" class="vs-img" />
</div>
<TeamEmblem
size="md"
:team-code="match.awayTeamCode"
:team-name="match.awayTeamName"
:logo-url="match.awayTeamLogoUrl"
/>
</div>
</div>
<div v-if="!loading && !hotMatches.length" class="empty">
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
<p>{{ t('home.no_matches') }}</p>
</div>
</div>
</template>
<style scoped>
.pull-indicator {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: height 0.15s ease;
}
.match-card {
position: relative;
isolation: isolate;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
padding: 14px 16px;
min-height: 72px;
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: none;
background: var(--bg-card);
cursor: pointer;
transition: opacity 0.2s, transform 0.2s;
overflow: hidden;
}
.match-card::before {
content: '';
position: absolute;
inset: 0;
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
opacity: 0.25;
z-index: 0;
pointer-events: none;
}
.match-card:active {
opacity: 0.92;
transform: scale(0.995);
}
.match-info,
.match-flags {
position: relative;
z-index: 1;
}
.match-info {
flex: 1;
min-width: 0;
}
.match-teams {
font-weight: 800;
margin-bottom: 8px;
font-size: 16px;
line-height: 1.3;
}
.match-time {
font-size: 13px;
color: var(--text-muted);
font-weight: 500;
}
.match-flags {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 4px;
max-width: 46%;
}
.vs-arena {
position: relative;
flex-shrink: 0;
width: 64px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
}
.hz-lightning {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
overflow: visible;
}
.hz-path {
fill: none;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 0 4px rgba(120, 210, 255, 0.95)) drop-shadow(0 0 8px rgba(255, 180, 80, 0.55));
opacity: 0;
}
.match-card--live-anim .hz-path-main {
animation: hz-strike-main 2.6s ease-in-out infinite;
}
.match-card--live-anim .hz-path-sub {
stroke-width: 1.6;
animation: hz-strike-sub 2.6s ease-in-out infinite;
animation-delay: 0.12s;
}
.match-card--live-anim .hz-beam {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 2px;
transform: translateY(-50%);
pointer-events: none;
z-index: 1;
background: linear-gradient(
90deg,
rgba(94, 184, 255, 0) 0%,
rgba(184, 236, 255, 0.95) 28%,
#fff 50%,
rgba(255, 208, 128, 0.95) 72%,
rgba(255, 144, 64, 0) 100%
);
opacity: 0;
filter: blur(0.4px);
animation: hz-beam-flash 2.6s ease-in-out infinite;
}
.hz-beam {
display: none;
}
.vs-img {
position: relative;
z-index: 0;
width: 48px;
height: auto;
object-fit: contain;
filter: drop-shadow(0 0 3px rgba(212, 175, 55, 0.35));
}
.match-card--live-anim .vs-img {
animation: vs-glow 2.4s ease-in-out infinite;
}
@keyframes hz-strike-main {
0%,
72%,
100% {
opacity: 0;
}
74% {
opacity: 1;
}
75% {
opacity: 0.25;
}
76% {
opacity: 0.95;
}
78% {
opacity: 0;
}
}
@keyframes hz-strike-sub {
0%,
74%,
100% {
opacity: 0;
}
76% {
opacity: 0.85;
}
77% {
opacity: 0.2;
}
78% {
opacity: 0.7;
}
80% {
opacity: 0;
}
}
@keyframes hz-beam-flash {
0%,
71%,
100% {
opacity: 0;
transform: translateY(-50%) scaleX(0.6);
}
73% {
opacity: 0.85;
transform: translateY(-50%) scaleX(1);
}
75% {
opacity: 0.15;
transform: translateY(-50%) scaleX(0.95);
}
76% {
opacity: 0.75;
transform: translateY(-50%) scaleX(1);
}
78% {
opacity: 0;
transform: translateY(-50%) scaleX(1.05);
}
}
@keyframes vs-glow {
0%,
100% {
opacity: 0.82;
filter: drop-shadow(0 0 2px rgba(212, 175, 55, 0.3));
}
50% {
opacity: 1;
filter:
drop-shadow(0 0 3px rgba(255, 230, 140, 0.7))
drop-shadow(0 0 6px rgba(212, 175, 55, 0.35));
}
}
@media (prefers-reduced-motion: reduce) {
.vs-img {
animation: none;
filter: drop-shadow(0 0 3px rgba(212, 175, 55, 0.35));
}
.hz-path,
.hz-beam {
animation: none;
opacity: 0;
}
}
.empty {
text-align: center;
color: var(--text-muted);
padding: 40px 20px;
font-weight: 600;
}
.empty-icon {
width: 96px;
height: 96px;
margin-bottom: 14px;
}
</style>