Files
thebet365/apps/admin/src/views/Matches.vue
Mars d8e08aaed0 sync(theme-3): 同步 main 最新 API/Admin 与玩家端逻辑
从 main 同步站内信手动发送、媒体库选择、列表子路由重构等 API/Admin 改动;玩家端仅合并 ADMIN_CUSTOM 富文本、消息预览与充值截图压缩逻辑,保留 theme-3 样式。
2026-06-22 14:11:47 +08:00

824 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, watch, onBeforeUnmount, onDeactivated } from 'vue';
defineOptions({ name: 'AdminMatches' });
import { useStaleListLifecycle } from '../composables/useStaleList';
import { useRoute, useRouter } from 'vue-router';
import { useAdminLocale } from '../composables/useAdminLocale';
import { resolveFormError } from '../i18n/form-validation';
import api from '../api';
import { ElMessage, ElMessageBox } from 'element-plus';
import MatchesSubNav from '../components/MatchesSubNav.vue';
import LeagueRowActions from '../components/LeagueRowActions.vue';
import CountryFlagSelect from '../components/outright/CountryFlagSelect.vue';
import LogoUrlField from '../components/LogoUrlField.vue';
import LeagueArchiveDialog from '../components/LeagueArchiveDialog.vue';
import { getBuiltinCountry } from '../data/builtinCountries';
import {
readMatchesListUiState,
writeMatchesListUiState,
} from '../utils/matchesListState';
import { formatAmount } from '../utils/format-amount';
import {
emptyMatchForm,
buildPlatformPayload,
fillBuiltinTeam,
clearBuiltinTeam,
type MatchCreateForm,
} from './match-form';
const { t, localeTag } = useAdminLocale();
const route = useRoute();
const router = useRouter();
const isMatchChildRoute = computed(() =>
/^\/matches\/leagues\/[^/]+/.test(route.path),
);
interface LeagueTableRow extends Record<string, unknown> {
id: string;
isPublished: boolean;
isPublishing: boolean;
labels: {
edit: string;
createFixture: string;
publish: string;
unpublish: string;
delete: string;
};
displaySeq: number;
displayNameZh: string;
displayNameEn: string;
displayStatusLabel: string;
displayStatusTagType: 'success' | 'info';
displayMatchCount: number;
displayBetCount: number;
displayBetCountActive: boolean;
displayTotalStake: string;
displayPendingBets: number;
displayCode: string;
}
const leagues = ref<LeagueTableRow[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(10);
const filterStatus = ref('');
const keyword = ref('');
const createLeagueVisible = ref(false);
const createLeagueLoading = ref(false);
const leagueDialogMode = ref<'create' | 'edit'>('create');
const leagueEditingId = ref('');
const leagueForm = ref({ leagueEn: '', leagueZh: '', leagueMs: '', logoUrl: '', deleteOldLogo: false, originalLogoUrl: '' });
const publishingLeagueId = ref('');
const leagueArchiveVisible = ref(false);
const leagueArchiveId = ref('');
const leagueArchiveName = ref('');
const leagueDialogTitle = computed(() =>
leagueDialogMode.value === 'edit'
? t('match.dialog.edit_league')
: t('match.dialog.create_league'),
);
const createVisible = ref(false);
const createLoading = ref(false);
const form = ref<MatchCreateForm>(emptyMatchForm());
const createUnderLeagueLabel = ref('');
const isFixtureCreate = computed(() => !!form.value.leagueId.trim());
function rowOf(row: unknown) {
return row as Record<string, unknown>;
}
function leagueId(row: unknown) {
return String(rowOf(row).id ?? '');
}
function leagueTitle(row: unknown) {
const r = rowOf(row);
const zh = String(r.leagueZh ?? '').trim();
const en = String(r.leagueEn ?? '').trim();
return zh || en || String(r.code ?? '—');
}
function leagueActionLabels() {
return {
edit: t('common.edit'),
createFixture: t('match.create_fixture_btn'),
publish: t('common.publish'),
unpublish: t('league.btn.unpublish'),
delete: t('common.delete'),
};
}
function mapLeagueRows(items: unknown[], publishingId = ''): LeagueTableRow[] {
const labels = leagueActionLabels();
const start = (page.value - 1) * pageSize.value;
return items.map((item, index) => {
const r = rowOf(item);
const id = String(r.id ?? '');
const published = Boolean(r.isPublished);
const stats = r.betStats as
| { betCount?: number; totalStake?: string; pendingCount?: number }
| undefined;
const betCount = Number(stats?.betCount ?? 0);
return {
...r,
id,
isPublished: published,
isPublishing: publishingId === id,
labels,
displaySeq: start + index + 1,
displayNameZh: String(r.leagueZh ?? '').trim() || '—',
displayNameEn: String(r.leagueEn ?? '').trim() || '—',
displayStatusLabel: published ? t('league.status.PUBLISHED') : t('league.status.UNPUBLISHED'),
displayStatusTagType: published ? 'success' : 'info',
displayMatchCount: Number(r.matchCount ?? 0),
displayBetCount: betCount,
displayBetCountActive: betCount > 0,
displayTotalStake: formatAmount(String(stats?.totalStake ?? '0')),
displayPendingBets: Number(stats?.pendingCount ?? 0),
displayCode: String(r.code ?? ''),
};
});
}
function remapLeagueRows(publishingId = publishingLeagueId.value) {
if (!leagues.value.length) return;
leagues.value = mapLeagueRows(leagues.value, publishingId);
}
function persistListUiState() {
writeMatchesListUiState({
page: page.value,
pageSize: pageSize.value,
filterStatus: filterStatus.value,
keyword: keyword.value,
});
}
type LoadOptions = { restore?: boolean };
async function load(options: LoadOptions = {}) {
const saved = options.restore ? readMatchesListUiState() : null;
if (saved) {
page.value = saved.page;
pageSize.value = saved.pageSize;
filterStatus.value = saved.filterStatus;
keyword.value = saved.keyword;
}
const { data } = await api.get('/admin/leagues', {
params: {
page: page.value,
pageSize: pageSize.value,
status: filterStatus.value || undefined,
keyword: keyword.value.trim() || undefined,
},
});
leagues.value = mapLeagueRows(data.data.items);
total.value = data.data.total;
persistListUiState();
}
function onSearch() {
page.value = 1;
void runLoad(true);
}
async function initialLoad() {
if (isMatchChildRoute.value) return;
const qStatus = route.query.status;
if (typeof qStatus === 'string' && qStatus.trim()) {
filterStatus.value = qStatus.trim();
page.value = 1;
await load();
return;
}
const qLeague = route.query.leagueId;
if (typeof qLeague === 'string' && qLeague.trim()) {
router.replace(`/matches/leagues/${qLeague.trim()}`);
return;
}
await load({ restore: true });
}
const { loading: listLoading, runLoad } = useStaleListLifecycle(() => leagues.value.length > 0, initialLoad);
onBeforeUnmount(persistListUiState);
onDeactivated(persistListUiState);
watch(localeTag, () => remapLeagueRows());
function onPageChange(p: number) {
page.value = p;
load();
}
function onSizeChange(size: number) {
pageSize.value = size;
page.value = 1;
load();
}
function openCreateLeague() {
leagueDialogMode.value = 'create';
leagueEditingId.value = '';
leagueForm.value = { leagueEn: '', leagueZh: '', leagueMs: '', logoUrl: '', deleteOldLogo: false, originalLogoUrl: '' };
createLeagueVisible.value = true;
}
function openEditLeague(row: unknown) {
const r = rowOf(row);
leagueDialogMode.value = 'edit';
leagueEditingId.value = String(r.id ?? '');
const currentLogoUrl = String(r.logoUrl ?? '');
leagueForm.value = {
leagueEn: String(r.leagueEn ?? ''),
leagueZh: String(r.leagueZh ?? ''),
leagueMs: String(r.leagueMs ?? ''),
logoUrl: currentLogoUrl,
deleteOldLogo: false,
originalLogoUrl: currentLogoUrl,
};
createLeagueVisible.value = true;
}
async function toggleLeaguePublish(row: unknown) {
const r = rowOf(row);
const id = String(r.id ?? '');
const published = leagueIsPublished(row);
if (published) {
try {
await ElMessageBox.confirm(t('league.confirm_unpublish'), t('common.confirm'), {
type: 'warning',
confirmButtonText: t('league.btn.unpublish'),
cancelButtonText: t('common.cancel'),
});
} catch {
return;
}
}
publishingLeagueId.value = id;
remapLeagueRows(id);
try {
await api.put(`/admin/leagues/${id}`, {
leagueEn: String(r.leagueEn ?? ''),
leagueZh: String(r.leagueZh ?? ''),
leagueMs: String(r.leagueMs ?? ''),
logoUrl: String(r.logoUrl ?? '').trim() || undefined,
isActive: !published,
});
ElMessage.success(published ? t('msg.league_unpublished') : t('msg.league_published'));
await load();
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
} finally {
publishingLeagueId.value = '';
}
}
async function submitLeagueForm() {
const { leagueEn, leagueZh, leagueMs, logoUrl, deleteOldLogo, originalLogoUrl } = leagueForm.value;
if (!leagueZh.trim() && !leagueEn.trim()) {
ElMessage.warning(t('err.league_required'));
return;
}
createLeagueLoading.value = true;
try {
const body = {
leagueEn: leagueEn.trim(),
leagueZh: leagueZh.trim(),
leagueMs: leagueMs.trim() || undefined,
logoUrl: logoUrl.trim() || undefined,
...(leagueDialogMode.value === 'create' ? { isActive: false } : {}),
};
if (leagueDialogMode.value === 'edit') {
await api.put(`/admin/leagues/${leagueEditingId.value}`, body);
ElMessage.success(t('msg.league_updated'));
} else {
await api.post('/admin/leagues', body);
ElMessage.success(t('msg.league_created'));
}
// Delete old resource if user checked the option and the URL actually changed
if (deleteOldLogo && originalLogoUrl && originalLogoUrl !== logoUrl.trim()) {
try {
await api.delete('/admin/uploads/by-url', { data: { url: originalLogoUrl } });
ElMessage.success('旧资源已删除');
} catch {
ElMessage.warning('旧资源删除失败,可稍后在媒体库中清理');
}
}
createLeagueVisible.value = false;
load();
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
} finally {
createLeagueLoading.value = false;
}
}
function onTeamCodeChange(side: 'home' | 'away', code: string) {
if (!code?.trim()) {
clearBuiltinTeam(form.value, side);
return;
}
const country = getBuiltinCountry(code);
if (country) fillBuiltinTeam(form.value, side, country);
}
function openCreateFixture(leagueRow: unknown) {
const r = rowOf(leagueRow);
form.value = emptyMatchForm();
form.value.leagueId = String(r.id ?? '');
form.value.leagueEn = String(r.leagueEn ?? '');
form.value.leagueZh = String(r.leagueZh ?? '');
form.value.leagueMs = String(r.leagueMs ?? '');
createUnderLeagueLabel.value = leagueTitle(leagueRow);
createVisible.value = true;
}
async function submitCreate() {
let payload: ReturnType<typeof buildPlatformPayload>;
try {
payload = buildPlatformPayload(form.value);
} catch (e) {
ElMessage.warning(resolveFormError(e, t));
return;
}
createLoading.value = true;
try {
await api.post('/admin/matches', payload);
ElMessage.success(t('msg.match_created_draft'));
createUnderLeagueLabel.value = '';
createVisible.value = false;
const lid = form.value.leagueId.trim();
await load();
if (lid) {
router.push({
path: `/matches/leagues/${lid}`,
query: {
...(filterStatus.value ? { status: filterStatus.value } : {}),
...(keyword.value.trim() ? { keyword: keyword.value.trim() } : {}),
title: createUnderLeagueLabel.value || leagueTitle({ id: lid }),
},
});
}
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? t('msg.create_failed'));
} finally {
createLoading.value = false;
}
}
function openLeaguePage(row: unknown, _column: unknown, event: Event) {
const target = event.target;
const el = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
if (!el) return;
if (el.closest('.league-row-actions') || el.closest('.el-button')) return;
const id = leagueId(row);
if (!id) return;
void router.push({
name: 'admin-league-matches',
params: { leagueId: id },
query: {
...(filterStatus.value ? { status: filterStatus.value } : {}),
...(keyword.value.trim() ? { keyword: keyword.value.trim() } : {}),
title: leagueTitle(row),
},
});
}
function rowClassName() {
return 'row-navigable';
}
function leagueIsPublished(row: unknown) {
return Boolean(rowOf(row).isPublished);
}
function openLeagueArchive(row: unknown) {
leagueArchiveId.value = leagueId(row);
leagueArchiveName.value = leagueTitle(row);
leagueArchiveVisible.value = true;
}
function onLeagueArchived() {
void load();
}
</script>
<template>
<div class="matches-shell">
<router-view v-if="isMatchChildRoute" />
<div v-else class="admin-list-page matches-page">
<div class="list-chrome">
<div class="list-chrome__row">
<div class="list-chrome__left">
<MatchesSubNav embedded />
<div class="list-chrome__filters">
<div class="list-chrome__field">
<span class="list-chrome__label">{{ t('common.keyword') }}</span>
<el-input
v-model="keyword"
:placeholder="t('match.filter.keyword_ph')"
clearable
style="width: 200px"
@keyup.enter="load"
/>
</div>
<div class="list-chrome__field">
<span class="list-chrome__label">{{ t('common.status') }}</span>
<el-select v-model="filterStatus" :placeholder="t('common.all')" clearable style="width: 120px">
<el-option :label="t('match.status.DRAFT')" value="DRAFT" />
<el-option :label="t('match.status.PUBLISHED')" value="PUBLISHED" />
<el-option :label="t('match.status.CLOSED')" value="CLOSED" />
<el-option :label="t('match.status.PENDING_SETTLEMENT')" value="PENDING_SETTLEMENT" />
<el-option :label="t('match.status.SETTLED')" value="SETTLED" />
</el-select>
</div>
<el-button type="primary" class="list-chrome__submit" @click="onSearch">
{{ t('common.search') }}
</el-button>
</div>
</div>
<div class="list-chrome__actions">
<el-button type="primary" @click.stop="openCreateLeague">
{{ t('match.create_btn') }}
</el-button>
</div>
</div>
<p v-if="filterStatus" class="list-hint">{{ t('match.filter.status_hint') }}</p>
</div>
<section v-loading="listLoading" class="list-panel">
<p class="list-hint">{{ t('match.open_league_hint') }}</p>
<div class="table-wrap">
<el-table
:data="leagues"
stripe
row-key="id"
:row-class-name="rowClassName"
@row-click="openLeaguePage"
>
<el-table-column prop="displaySeq" :label="t('common.seq')" width="70" align="center" />
<el-table-column width="40" align="center" class-name="league-logo-cell">
<template #default="{ row }">
<img
v-if="row.logoUrl"
:src="String(row.logoUrl)"
alt=""
class="league-logo"
/>
</template>
</el-table-column>
<el-table-column
prop="displayNameZh"
:label="t('match.col.league')"
width="108"
show-overflow-tooltip
class-name="league-name-cell"
/>
<el-table-column prop="displayNameEn" :label="t('match.col.league_en')" width="180" show-overflow-tooltip class-name="league-en-cell" />
<el-table-column :label="t('common.status')" width="88" align="center">
<template #default="{ row }">
<el-tag :type="row.displayStatusTagType" size="small" effect="plain">
{{ row.displayStatusLabel }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="displayMatchCount" :label="t('match.col.fixture_count')" width="88" align="center" />
<el-table-column :label="t('match.col.bet_count')" width="72" align="center">
<template #default="{ row }">
<span :class="{ 'bet-stat-active': row.displayBetCountActive }">{{ row.displayBetCount }}</span>
</template>
</el-table-column>
<el-table-column prop="displayTotalStake" :label="t('match.col.total_stake')" width="108" align="right" />
<el-table-column :label="t('match.col.pending_bets')" width="80" align="center">
<template #default="{ row }">
<el-tag v-if="row.displayPendingBets > 0" type="warning" size="small" effect="plain">
{{ row.displayPendingBets }}
</el-tag>
<span v-else class="bet-stat-zero">0</span>
</template>
</el-table-column>
<el-table-column prop="displayCode" :label="t('match.col.league_code')" width="120" show-overflow-tooltip />
<el-table-column :label="t('common.actions')" width="280" align="center" fixed="right">
<template #default="{ row }">
<LeagueRowActions
:row="row"
@edit="() => openEditLeague(row)"
@create-fixture="() => openCreateFixture(row)"
@toggle-publish="() => toggleLeaguePublish(row)"
@archive="() => openLeagueArchive(row)"
/>
</template>
</el-table-column>
</el-table>
</div>
<div class="pager">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
background
@current-change="onPageChange"
@size-change="onSizeChange"
/>
</div>
</section>
<el-dialog v-model="createLeagueVisible" :title="leagueDialogTitle" width="520px" destroy-on-close>
<el-form label-width="96px">
<el-form-item :label="t('match.field.league_zh')">
<el-input v-model="leagueForm.leagueZh" :placeholder="t('match.ph.league_zh')" />
</el-form-item>
<el-form-item :label="t('match.field.league_en')">
<el-input v-model="leagueForm.leagueEn" :placeholder="t('match.ph.league_en')" />
</el-form-item>
<el-form-item :label="t('match.field.league_ms')">
<el-input v-model="leagueForm.leagueMs" :placeholder="t('match.ph.league_ms')" />
</el-form-item>
<el-form-item :label="t('match.field.league_logo')">
<LogoUrlField
v-model="leagueForm.logoUrl"
v-model:delete-old="leagueForm.deleteOldLogo"
upload-only
upload-category="banners"
/>
</el-form-item>
<p v-if="leagueDialogMode === 'create'" class="field-hint">{{ t('match.hint.create_league') }}</p>
</el-form>
<template #footer>
<el-button @click="createLeagueVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" :loading="createLeagueLoading" @click="submitLeagueForm">
{{ leagueDialogMode === 'edit' ? t('common.save') : t('user.btn.create') }}
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="createVisible"
:title="t('match.dialog.create_fixture')"
width="860px"
destroy-on-close
>
<el-form label-width="96px">
<el-form-item v-if="isFixtureCreate" :label="t('match.col.league')">
<span class="league-readonly">{{ createUnderLeagueLabel }}</span>
</el-form-item>
<template v-else>
<el-form-item :label="t('match.field.league_en')">
<el-input v-model="form.leagueEn" :placeholder="t('match.ph.league_en')" />
</el-form-item>
<el-form-item :label="t('match.field.league_zh')">
<el-input v-model="form.leagueZh" :placeholder="t('match.ph.league_zh')" />
</el-form-item>
</template>
<el-form-item :label="t('match.field.kickoff')" required>
<el-date-picker
v-model="form.startTime"
type="datetime"
value-format="YYYY-MM-DDTHH:mm:ss"
:placeholder="t('matchEditor.ph.kickoff')"
style="width: 100%"
/>
<p class="field-hint schedule-timezone-hint">{{ t('match.timezone.platform_hint') }}</p>
</el-form-item>
<div class="teams-row">
<!-- Home Team Column -->
<div class="team-col">
<div class="team-col-title">{{ t('match.field.home_team') }}</div>
<el-form-item :label="t('match.field.home_team')" required>
<CountryFlagSelect
v-model="form.homeTeamCode"
size="default"
class="team-country-select"
@update:model-value="onTeamCodeChange('home', $event)"
/>
</el-form-item>
<el-form-item :label="t('match.field.home_en')" label-width="108px">
<el-input v-model="form.homeTeamEn" :placeholder="t('match.ph.home_en')" />
</el-form-item>
<el-form-item :label="t('match.field.home_zh')" label-width="108px">
<el-input v-model="form.homeTeamZh" :placeholder="t('match.ph.home_zh')" />
</el-form-item>
<el-form-item :label="t('match.field.home_ms')" label-width="108px">
<el-input v-model="form.homeTeamMs" :placeholder="t('match.ph.home_ms')" />
</el-form-item>
<el-form-item :label="t('matchEditor.field.home_logo')" label-width="108px">
<LogoUrlField v-model="form.homeTeamLogoUrl" :team-code="form.homeTeamCode" upload-category="teams" />
</el-form-item>
</div>
<!-- Away Team Column -->
<div class="team-col">
<div class="team-col-title">{{ t('match.field.away_team') }}</div>
<el-form-item :label="t('match.field.away_team')" required>
<CountryFlagSelect
v-model="form.awayTeamCode"
size="default"
class="team-country-select"
@update:model-value="onTeamCodeChange('away', $event)"
/>
</el-form-item>
<el-form-item :label="t('match.field.away_en')" label-width="108px">
<el-input v-model="form.awayTeamEn" :placeholder="t('match.ph.away_en')" />
</el-form-item>
<el-form-item :label="t('match.field.away_zh')" label-width="108px">
<el-input v-model="form.awayTeamZh" :placeholder="t('match.ph.away_zh')" />
</el-form-item>
<el-form-item :label="t('match.field.away_ms')" label-width="108px">
<el-input v-model="form.awayTeamMs" :placeholder="t('match.ph.away_ms')" />
</el-form-item>
<el-form-item :label="t('matchEditor.field.away_logo')" label-width="108px">
<LogoUrlField v-model="form.awayTeamLogoUrl" :team-code="form.awayTeamCode" upload-category="teams" />
</el-form-item>
</div>
</div>
<el-form-item :label="t('match.field.featured')">
<el-switch v-model="form.isHot" />
</el-form-item>
<p class="field-hint">{{ t('match.hint.create_draft') }}</p>
</el-form>
<template #footer>
<el-button @click="createVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" :loading="createLoading" @click="submitCreate">{{ t('user.btn.create') }}</el-button>
</template>
</el-dialog>
<LeagueArchiveDialog
v-model="leagueArchiveVisible"
:league-id="leagueArchiveId"
:league-name="leagueArchiveName"
@archived="onLeagueArchived"
/>
</div>
</div>
</template>
<style scoped>
.matches-shell {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.matches-shell > :deep(.league-matches-page) {
flex: 1;
min-height: 0;
}
.team-country-select {
width: 100%;
}
.teams-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 20px;
}
.team-col {
display: flex;
flex-direction: column;
min-width: 0;
}
.team-col-title {
font-size: 14px;
font-weight: 750;
color: var(--text);
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid var(--border-soft);
}
.field-hint {
font-size: 12px;
color: var(--text-muted);
margin: 0;
line-height: 1.5;
}
.schedule-timezone-hint {
margin-top: 4px;
}
.edit-hint {
margin-bottom: 16px;
}
/* 列表表格随内容增高,滚动交给外层 table-wrap仅赛事行 */
.matches-page .table-wrap .el-table {
height: auto !important;
}
.matches-page .table-wrap :deep(.el-table__header),
.matches-page .table-wrap :deep(.el-table__body) {
width: 100% !important;
}
.list-panel :deep(.row-navigable) {
cursor: pointer;
}
.matchup-link {
color: var(--green-text);
}
.bet-stat-active {
color: var(--green-text);
font-weight: 600;
}
.bet-stat-zero {
color: #aaa49a;
}
.league-logo {
width: 28px;
height: 28px;
object-fit: contain;
flex-shrink: 0;
}
.matches-page .table-wrap :deep(.league-name-cell .cell) {
color: var(--primary-link);
font-weight: 600;
}
.matches-page .table-wrap :deep(.league-logo-cell .cell) {
padding: 0 4px;
}
.league-en {
color: var(--text-muted);
font-size: 13px;
}
.league-readonly {
color: var(--success-text);
font-weight: 700;
}
.matches-page .table-wrap :deep(.el-table__header .el-table__cell) {
padding: 6px 0;
}
.matches-page .table-wrap :deep(.el-table__header .cell) {
overflow: visible;
}
:deep(.logo-url-field) {
width: 100%;
}
.list-chrome__actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-shrink: 0;
}
.team-col {
padding: 12px;
border: 1px solid var(--border-soft);
border-radius: 8px;
background: #fbfaf7;
}
.matchup-link,
.bet-stat-active {
color: var(--primary-link);
}
.league-row-actions :deep(.el-button) {
min-height: 26px;
font-weight: 700;
}
@media (max-width: 760px) {
.list-chrome__actions {
width: 100%;
justify-content: flex-start;
}
.teams-row {
grid-template-columns: 1fr;
gap: 12px;
}
}
</style>