feat(theme-2): sync match search feature from main

This commit is contained in:
2026-06-18 09:45:53 +08:00
parent e56029225e
commit 740799a820

View File

@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onActivated, watch } from 'vue'; import { ref, computed, onActivated, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { usePlayerMatches } from '../composables/usePlayerMatches'; import { usePlayerMatches } from '../composables/usePlayerMatches';
import LeagueAccordionItem from '../components/LeagueAccordionItem.vue'; import LeagueAccordionItem from '../components/LeagueAccordionItem.vue';
import MatchBetCard from '../components/MatchBetCard.vue';
import OutrightPanel from '../components/outright/OutrightPanel.vue'; import OutrightPanel from '../components/outright/OutrightPanel.vue';
import emptyMatchesImg from '../assets/images/empty-matches.svg'; import emptyMatchesImg from '../assets/images/empty-matches.svg';
import { useOnLocaleChange } from '../composables/useOnLocaleChange'; import { useOnLocaleChange } from '../composables/useOnLocaleChange';
@@ -59,8 +60,10 @@ interface LeagueGroup {
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const route = useRoute();
const mainTab = ref<MainTab>('matches'); const mainTab = ref<MainTab>('matches');
const searchQuery = ref('');
const filterState = ref({ const filterState = ref({
time: 'all' as 'all' | 'today' | 'early', time: 'all' as 'all' | 'today' | 'early',
status: 'open' as 'all' | 'open' | 'settled', status: 'open' as 'all' | 'open' | 'settled',
@@ -96,10 +99,23 @@ function normalizeLeagueName(name: string): string {
.trim(); .trim();
} }
function syncSearchFromRoute() {
const q = route.query.search;
searchQuery.value = typeof q === 'string' ? q : '';
}
function matchesSearchKeyword(m: Match, keyword: string) {
if (!keyword) return true;
const haystack = `${m.homeTeamName} ${m.awayTeamName} ${m.leagueName}`.toLowerCase();
return haystack.includes(keyword);
}
const filteredMatches = computed(() => { const filteredMatches = computed(() => {
if (mainTab.value !== 'matches') return []; if (mainTab.value !== 'matches') return [];
const now = filterNow.value; const now = filterNow.value;
const keyword = searchQuery.value.trim().toLowerCase();
return matches.value.filter((m) => { return matches.value.filter((m) => {
if (!matchesSearchKeyword(m, keyword)) return false;
let timeMatch = true; let timeMatch = true;
if (filterState.value.time === 'today') { if (filterState.value.time === 'today') {
timeMatch = isInTodayMatchWindow(m.startTime, now); timeMatch = isInTodayMatchWindow(m.startTime, now);
@@ -196,6 +212,16 @@ function buildLeagueGroups(source: Match[]): LeagueGroup[] {
const leagueGroups = computed(() => buildLeagueGroups(filteredMatches.value)); const leagueGroups = computed(() => buildLeagueGroups(filteredMatches.value));
const isSearchActive = computed(() => searchQuery.value.trim().length > 0);
const sortedFilteredMatches = computed(() =>
[...filteredMatches.value].sort(
(a, b) =>
(a.displayOrder ?? 0) - (b.displayOrder ?? 0) ||
new Date(a.startTime).getTime() - new Date(b.startTime).getTime(),
),
);
const expandedLeagues = ref(new Set<string>()); const expandedLeagues = ref(new Set<string>());
watch(leagueGroups, (groups) => { watch(leagueGroups, (groups) => {
@@ -226,9 +252,15 @@ function selectMainTab(tab: MainTab) {
onActivated(() => { onActivated(() => {
filterNow.value = new Date(); filterNow.value = new Date();
filterState.value.time = 'all'; filterState.value.time = 'all';
syncSearchFromRoute();
void loadSummary(true, true); void loadSummary(true, true);
}); });
watch(
() => route.query.search,
() => syncSearchFromRoute(),
);
function goMatch(id: string) { function goMatch(id: string) {
router.push(`/match/${id}`); router.push(`/match/${id}`);
} }
@@ -265,6 +297,17 @@ function goMatch(id: string) {
</div> </div>
<div :class="['tab-panel', { 'tab-panel--hidden': mainTab !== 'matches' }]"> <div :class="['tab-panel', { 'tab-panel--hidden': mainTab !== 'matches' }]">
<div class="search-bar">
<span class="search-icon" aria-hidden="true"></span>
<input
v-model="searchQuery"
type="search"
:placeholder="t('search.placeholder')"
enterkeyhint="search"
autocomplete="off"
/>
</div>
<div class="filters-bar"> <div class="filters-bar">
<div class="phase-filter"> <div class="phase-filter">
<div class="league-dropdown"> <div class="league-dropdown">
@@ -340,7 +383,22 @@ function goMatch(id: string) {
</div> </div>
<template v-else> <template v-else>
<div> <div>
<div v-if="leagueGroups.length" class="league-list"> <div v-if="isSearchActive && sortedFilteredMatches.length" class="search-results">
<p class="search-results-meta">
{{ t('search.results_count', { count: sortedFilteredMatches.length }) }}
</p>
<div class="search-match-list">
<article
v-for="match in sortedFilteredMatches"
:key="match.id"
class="search-result-item"
>
<p class="search-result-league">{{ normalizeLeagueName(match.leagueName) }}</p>
<MatchBetCard :match="match" @bet="goMatch" />
</article>
</div>
</div>
<div v-else-if="!isSearchActive && leagueGroups.length" class="league-list">
<LeagueAccordionItem <LeagueAccordionItem
v-for="group in leagueGroups" v-for="group in leagueGroups"
:key="group.leagueId" :key="group.leagueId"
@@ -355,7 +413,7 @@ function goMatch(id: string) {
</div> </div>
<div v-else class="empty"> <div v-else class="empty">
<img :src="emptyMatchesImg" alt="" class="empty-icon" /> <img :src="emptyMatchesImg" alt="" class="empty-icon" />
<p>{{ t('bet.no_matches') }}</p> <p>{{ isSearchActive ? t('search.no_results') : t('bet.no_matches') }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -637,4 +695,75 @@ function goMatch(id: string) {
.outright-tab { .outright-tab {
min-height: 0; min-height: 0;
} }
.search-bar {
display: flex;
align-items: center;
gap: 8px;
margin: 0 12px 8px;
padding: 0 10px;
height: 32px;
box-sizing: border-box;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-card);
}
.search-icon {
color: var(--primary-light);
font-size: 14px;
line-height: 1;
flex-shrink: 0;
}
.search-bar input {
flex: 1;
min-width: 0;
height: 100%;
border: none;
background: transparent;
color: var(--text);
font-size: 12px;
font-weight: 500;
line-height: 1.2;
outline: none;
-webkit-appearance: none;
appearance: none;
}
.search-bar input::placeholder {
color: var(--text-muted);
}
.search-results {
padding: 0 12px;
}
.search-results-meta {
margin: 0 0 8px;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
}
.search-match-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.search-result-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.search-result-league {
margin: 0;
padding: 0 2px;
font-size: 11px;
font-weight: 700;
color: var(--primary-light);
line-height: 1.3;
}
</style> </style>