feat(player): theme-3 移动端/PC 双端拆分与桌面投注工作台
新增 DesktopShell 及桌面端首页、赛事、钱包、记录等页面,原 H5 视图迁移至 Mobile* 并由路由 wrapper 按视口切换。补齐右侧投注单栏、赔率快捷浮层、联赛/赛事侧栏、串关与定位能力;桌面下注成功改为全局 Toast,修复侧栏成功遮罩被裁剪与底部汇总黑底。同步悬浮客服、公告卡片、三语 i18n、桌面样式体系,以及 API 赛事与 shared 包相关调整。
This commit is contained in:
758
apps/player/src/views/MobileFootballView.vue
Normal file
758
apps/player/src/views/MobileFootballView.vue
Normal file
@@ -0,0 +1,758 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onActivated, watch } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { usePlayerMatches } from '../composables/usePlayerMatches';
|
||||
import LeagueAccordionItem from '../components/LeagueAccordionItem.vue';
|
||||
import MatchBetCard from '../components/MatchBetCard.vue';
|
||||
import OutrightPanel from '../components/outright/OutrightPanel.vue';
|
||||
import emptyMatchesImg from '../assets/images/empty-matches.svg';
|
||||
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import type { MatchPhase } from '../utils/matchPhase';
|
||||
import {
|
||||
isAfterLocalTodayMatchWindow as isAfterTodayMatchWindow,
|
||||
isInLocalTodayMatchWindow as isInTodayMatchWindow,
|
||||
} from '@thebet365/shared';
|
||||
|
||||
type MainTab = 'matches' | 'outright';
|
||||
|
||||
interface Match {
|
||||
id: string;
|
||||
leagueId?: string;
|
||||
homeTeamName: string;
|
||||
awayTeamName: string;
|
||||
homeTeamCode?: string;
|
||||
awayTeamCode?: string;
|
||||
homeTeamLogoUrl?: string | null;
|
||||
awayTeamLogoUrl?: string | null;
|
||||
startTime: string;
|
||||
leagueName: string;
|
||||
leagueLogoUrl?: string | null;
|
||||
displayOrder?: number;
|
||||
isHot?: boolean;
|
||||
status?: string;
|
||||
bettingOpen?: boolean;
|
||||
matchPhase?: MatchPhase;
|
||||
score?: {
|
||||
htHome: number | null;
|
||||
htAway: number | null;
|
||||
ftHome: number | null;
|
||||
ftAway: number | null;
|
||||
homeCorners?: number | null;
|
||||
awayCorners?: number | null;
|
||||
homeYellowCards?: number | null;
|
||||
awayYellowCards?: number | null;
|
||||
homeRedCards?: number | null;
|
||||
awayRedCards?: number | null;
|
||||
homeCards?: number | null;
|
||||
awayCards?: number | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface LeagueGroup {
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
leagueLogoUrl?: string | null;
|
||||
matches: Match[];
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const mainTab = ref<MainTab>('matches');
|
||||
const searchQuery = ref('');
|
||||
const filterState = ref({
|
||||
time: 'all' as 'all' | 'today' | 'early',
|
||||
status: 'open' as 'all' | 'open' | 'settled',
|
||||
leagueIds: [] as string[]
|
||||
});
|
||||
const outrightActivated = ref(false);
|
||||
const filterNow = ref(new Date());
|
||||
const { summaryMatches, summaryLoading, loadSummary } = usePlayerMatches();
|
||||
const matches = summaryMatches;
|
||||
const loading = summaryLoading;
|
||||
|
||||
async function loadMatches() {
|
||||
filterNow.value = new Date();
|
||||
await loadSummary(true);
|
||||
}
|
||||
|
||||
useOnLocaleChange(() => loadSummary(true));
|
||||
|
||||
const { pullDistance, refreshing, spinning, progress } = usePullToRefresh({
|
||||
onRefresh: async () => { await loadMatches(); },
|
||||
});
|
||||
|
||||
const pullIndicatorStyle = () => ({
|
||||
height: `${pullDistance.value}px`,
|
||||
opacity: Math.min(pullDistance.value / 48, 1),
|
||||
});
|
||||
|
||||
function normalizeLeagueName(name: string): string {
|
||||
return name
|
||||
.replace(/\.unit$/i, '')
|
||||
.replace(/[-_]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.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(() => {
|
||||
if (mainTab.value !== 'matches') return [];
|
||||
const now = filterNow.value;
|
||||
const keyword = searchQuery.value.trim().toLowerCase();
|
||||
return matches.value.filter((m) => {
|
||||
if (!matchesSearchKeyword(m, keyword)) return false;
|
||||
let timeMatch = true;
|
||||
if (filterState.value.time === 'today') {
|
||||
timeMatch = isInTodayMatchWindow(m.startTime, now);
|
||||
} else if (filterState.value.time === 'early') {
|
||||
timeMatch = isAfterTodayMatchWindow(m.startTime, now);
|
||||
}
|
||||
if (!timeMatch) return false;
|
||||
|
||||
if (filterState.value.status === 'open' && m.matchPhase !== 'open' && m.matchPhase !== undefined) return false;
|
||||
if (filterState.value.status === 'settled' && m.matchPhase !== 'settled') return false;
|
||||
|
||||
if (filterState.value.leagueIds.length > 0) {
|
||||
const id = m.leagueId ?? m.leagueName;
|
||||
if (!filterState.value.leagueIds.includes(id)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const availableLeagues = computed(() => {
|
||||
const map = new Map<string, { id: string, name: string }>();
|
||||
for (const m of matches.value) {
|
||||
let timeMatch = true;
|
||||
if (filterState.value.time === 'today') {
|
||||
timeMatch = isInTodayMatchWindow(m.startTime, filterNow.value);
|
||||
} else if (filterState.value.time === 'early') {
|
||||
timeMatch = isAfterTodayMatchWindow(m.startTime, filterNow.value);
|
||||
}
|
||||
if (!timeMatch) continue;
|
||||
|
||||
const id = m.leagueId ?? m.leagueName;
|
||||
if (!map.has(id)) {
|
||||
map.set(id, { id, name: normalizeLeagueName(m.leagueName) });
|
||||
}
|
||||
}
|
||||
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
watch(() => filterState.value.time, () => {
|
||||
filterState.value.leagueIds = [];
|
||||
});
|
||||
|
||||
const dropdownOpen = ref(false);
|
||||
|
||||
const selectedLeagueName = computed(() => {
|
||||
if (filterState.value.leagueIds.length === 0) return '';
|
||||
const firstId = filterState.value.leagueIds[0];
|
||||
const lg = availableLeagues.value.find(l => l.id === firstId);
|
||||
return lg ? lg.name : '';
|
||||
});
|
||||
|
||||
function selectLeague(id: string) {
|
||||
if (id === '') {
|
||||
filterState.value.leagueIds = [];
|
||||
} else {
|
||||
filterState.value.leagueIds = [id];
|
||||
}
|
||||
dropdownOpen.value = false;
|
||||
}
|
||||
|
||||
function toggleStatusOpen() {
|
||||
filterState.value.status = filterState.value.status === 'open' ? 'all' : 'open';
|
||||
}
|
||||
|
||||
function buildLeagueGroups(source: Match[]): LeagueGroup[] {
|
||||
const map = new Map<string, LeagueGroup>();
|
||||
for (const m of source) {
|
||||
const id = m.leagueId ?? m.leagueName;
|
||||
if (!map.has(id)) {
|
||||
map.set(id, {
|
||||
leagueId: id,
|
||||
leagueName: normalizeLeagueName(m.leagueName),
|
||||
leagueLogoUrl: m.leagueLogoUrl ?? null,
|
||||
matches: [],
|
||||
});
|
||||
}
|
||||
map.get(id)!.matches.push(m);
|
||||
}
|
||||
const groups = [...map.values()];
|
||||
for (const g of groups) {
|
||||
g.matches.sort(
|
||||
(a, b) =>
|
||||
(a.displayOrder ?? 0) - (b.displayOrder ?? 0) ||
|
||||
new Date(a.startTime).getTime() - new Date(b.startTime).getTime(),
|
||||
);
|
||||
}
|
||||
return groups.sort(
|
||||
(a, b) =>
|
||||
(a.matches[0]?.displayOrder ?? 0) - (b.matches[0]?.displayOrder ?? 0) ||
|
||||
a.leagueName.localeCompare(b.leagueName),
|
||||
);
|
||||
}
|
||||
|
||||
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>());
|
||||
|
||||
watch(leagueGroups, (groups) => {
|
||||
const ids = new Set(expandedLeagues.value);
|
||||
for (const id of [...ids]) {
|
||||
if (!groups.some((g) => g.leagueId === id)) ids.delete(id);
|
||||
}
|
||||
if (groups.length > 0 && ids.size === 0) {
|
||||
ids.add(groups[0].leagueId);
|
||||
}
|
||||
if (ids.size !== expandedLeagues.value.size || [...ids].some((id) => !expandedLeagues.value.has(id))) {
|
||||
expandedLeagues.value = ids;
|
||||
}
|
||||
});
|
||||
|
||||
function toggleLeague(leagueId: string) {
|
||||
const next = new Set(expandedLeagues.value);
|
||||
if (next.has(leagueId)) next.delete(leagueId);
|
||||
else next.add(leagueId);
|
||||
expandedLeagues.value = next;
|
||||
}
|
||||
|
||||
function selectMainTab(tab: MainTab) {
|
||||
mainTab.value = tab;
|
||||
if (tab === 'outright') outrightActivated.value = true;
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
filterNow.value = new Date();
|
||||
filterState.value.time = 'all';
|
||||
syncSearchFromRoute();
|
||||
void loadSummary(true, true);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query.search,
|
||||
() => syncSearchFromRoute(),
|
||||
);
|
||||
|
||||
function goMatch(id: string) {
|
||||
router.push(`/match/${id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bet-page">
|
||||
<div
|
||||
class="pull-indicator"
|
||||
:style="pullIndicatorStyle()"
|
||||
>
|
||||
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
|
||||
</div>
|
||||
|
||||
<div class="main-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="main-tab"
|
||||
:class="{ active: mainTab === 'matches', 'tab-gold-active': mainTab === 'matches' }"
|
||||
@click="selectMainTab('matches')"
|
||||
>
|
||||
<span class="tab-icon">⚽</span>
|
||||
{{ t('bet.tab_matches') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="main-tab"
|
||||
:class="{ active: mainTab === 'outright', 'tab-gold-active': mainTab === 'outright' }"
|
||||
@click="selectMainTab('outright')"
|
||||
>
|
||||
<span class="tab-icon">🏆</span>
|
||||
{{ t('bet.tab_outright') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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="phase-filter">
|
||||
<div class="league-dropdown">
|
||||
<button type="button" class="dropdown-trigger" @click="dropdownOpen = !dropdownOpen">
|
||||
<span>{{ selectedLeagueName || t('bet.filter_leagues_all') }}</span>
|
||||
<span class="arrow-icon" :class="{ open: dropdownOpen }">▼</span>
|
||||
</button>
|
||||
<div v-if="dropdownOpen" class="dropdown-backdrop" @click="dropdownOpen = false"></div>
|
||||
<div v-if="dropdownOpen" class="dropdown-menu">
|
||||
<div
|
||||
class="dropdown-item"
|
||||
:class="{ active: filterState.leagueIds.length === 0 }"
|
||||
@click="selectLeague('')"
|
||||
>
|
||||
{{ t('bet.filter_leagues_all') }}
|
||||
</div>
|
||||
<div
|
||||
v-for="lg in availableLeagues"
|
||||
:key="lg.id"
|
||||
class="dropdown-item"
|
||||
:class="{ active: filterState.leagueIds.includes(lg.id) }"
|
||||
@click="selectLeague(lg.id)"
|
||||
>
|
||||
{{ lg.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-filters">
|
||||
<div class="time-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="time-tab"
|
||||
:class="{ active: filterState.time === 'all' }"
|
||||
@click="filterState.time = 'all'"
|
||||
>
|
||||
{{ t('bet.filter_status_all') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="time-tab"
|
||||
:class="{ active: filterState.time === 'today' }"
|
||||
@click="filterState.time = 'today'"
|
||||
>
|
||||
{{ t('bet.tab_today') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="time-tab"
|
||||
:class="{ active: filterState.time === 'early' }"
|
||||
@click="filterState.time = 'early'"
|
||||
>
|
||||
{{ t('bet.tab_early') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="status-toggle-btn"
|
||||
:class="{ active: filterState.status === 'open' }"
|
||||
@click="toggleStatusOpen"
|
||||
>
|
||||
{{ t('bet.filter_status_open') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="state">
|
||||
<GoldSpinner :size="36" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div>
|
||||
<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
|
||||
v-for="group in leagueGroups"
|
||||
:key="group.leagueId"
|
||||
:league-id="group.leagueId"
|
||||
:league-name="group.leagueName"
|
||||
:league-logo-url="group.leagueLogoUrl"
|
||||
:matches="group.matches"
|
||||
:expanded="expandedLeagues.has(group.leagueId)"
|
||||
@toggle="toggleLeague(group.leagueId)"
|
||||
@bet="goMatch"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="empty">
|
||||
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
|
||||
<p>{{ isSearchActive ? t('search.no_results') : t('bet.no_matches') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<OutrightPanel
|
||||
v-if="outrightActivated"
|
||||
:class="['outright-tab', 'tab-panel', { 'tab-panel--hidden': mainTab !== 'outright' }]"
|
||||
:activated="mainTab === 'outright'"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pull-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: height 0.15s ease;
|
||||
}
|
||||
|
||||
.bet-page {
|
||||
margin: 0 -16px;
|
||||
padding-bottom: 8px;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
/* Tab panel toggle: avoid display:none to prevent browser releasing image resources */
|
||||
.tab-panel {
|
||||
display: block;
|
||||
}
|
||||
.tab-panel--hidden {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.main-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
|
||||
.main-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 10px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
box-shadow: var(--shadow);
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.main-tab.active {
|
||||
font-weight: 700;
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.filters-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
|
||||
.phase-filter {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.time-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.time-tab {
|
||||
padding: 0 8px;
|
||||
height: 100%;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.time-tab.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(244, 162, 97, 0.15);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.right-filters .divider {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.status-toggle-btn {
|
||||
padding: 0 8px;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-toggle-btn.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(244, 162, 97, 0.15);
|
||||
border-color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* League Dropdown Selector */
|
||||
.league-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dropdown-trigger .arrow-icon {
|
||||
font-size: 8px;
|
||||
color: var(--text-muted);
|
||||
transition: transform 0.2s ease;
|
||||
display: inline-block;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.dropdown-trigger .arrow-icon.open {
|
||||
transform: scale(0.8) rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 98;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
min-width: 140px;
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-card-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item:active {
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.league-list {
|
||||
padding: 4px 12px 0;
|
||||
}
|
||||
|
||||
.state,
|
||||
.placeholder,
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 48px 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.outright-tab {
|
||||
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 {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-match-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.search-result-league {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-light);
|
||||
line-height: 1.3;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user