API: - 新增 player-messages 域:充值审核通过/拒绝、Banner/公告推广消息,支持多语言模板 - 新增 presence 域:Redis 心跳在线状态,管理端可查询在线玩家数 - User 表增加 visible_menus 字段;新增 player_messages 表及迁移 - 充值审核通过/拒绝时按系统配置自动写入玩家站内消息 - 管理端新增 GET /deposit-orders/pending-count、GET /presence/online-count - 玩家端新增消息 CRUD、presence/ping、home 返回 inbox 开关配置 - 员工管理支持 visibleMenus 配置与删除保护(不能删自己/最后超管) - SystemConfig 增加 inbox 功能开关及各类通知开关 Admin: - 员工管理:按角色默认菜单 + 可勾选可见菜单项 - ManageLayout:按 visibleMenus 过滤侧栏;充值待审数量角标轮询 - Contents:富文本编辑器、图片字段组件重构 - DashboardPlayers:展示在线玩家数;AdminPlayerStatusCell 在线状态列 - 多页面 i18n 与权限细节调整 Player: - 站内邮箱中心(InboxHub):消息列表/详情、未读角标、一键已读/删除 - 公告列表与详情页;走马灯可跳转详情 - 客服 Modal 改为 Panel,与邮箱 Hub 整合 - 充值状态轮询通知;presence 心跳;BetSlip 清空二次确认 - HomeView 今日赛事板块;FootballView 等体验优化 Shared: 新增 CANNOT_DELETE_SELF、STAFF_NOT_FOUND、MESSAGE_NOT_FOUND 等错误码 Docs: 玩家端缺失功能分析文档 Chore: 移除 .agents/skills 设计类 skill 文件 Co-authored-by: Cursor <cursoragent@cursor.com>
785 lines
19 KiB
Vue
785 lines
19 KiB
Vue
<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 class="dropdown-text">{{ 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;
|
|
}
|
|
|
|
.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(--gold);
|
|
font-size: 14px;
|
|
line-height: 1;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.search-bar input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
height: 100%;
|
|
border: none;
|
|
background: transparent;
|
|
color: #fff;
|
|
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);
|
|
}
|
|
|
|
.filters-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
gap: 8px;
|
|
padding: 0 12px 12px;
|
|
}
|
|
|
|
.phase-filter {
|
|
padding: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.right-filters {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.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;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.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;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.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;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.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;
|
|
width: 100%;
|
|
}
|
|
|
|
.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;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.dropdown-text {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.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: 170px;
|
|
max-width: 240px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
background: #0f1217;
|
|
border: 1px solid #2a3240;
|
|
border-radius: 8px;
|
|
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.65);
|
|
padding: 6px 0;
|
|
}
|
|
|
|
.dropdown-item {
|
|
padding: 8px 14px;
|
|
font-size: 13px;
|
|
color: #e5e7eb;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
border-bottom: 1px solid #1f2937;
|
|
}
|
|
|
|
.dropdown-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.dropdown-item:hover,
|
|
.dropdown-item:active {
|
|
background: #1b2330;
|
|
}
|
|
|
|
.dropdown-item.active {
|
|
color: #f4d06f;
|
|
background: #242f42;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.league-list {
|
|
padding: 4px 12px 0;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
</style>
|