feat(admin,api,player): settlement stats, team crests, MS fields and list bet summary

This commit is contained in:
2026-06-04 17:30:48 +08:00
parent cc737e2924
commit 9fcee31a9a
27 changed files with 2296 additions and 427 deletions

View File

@@ -4,9 +4,8 @@ import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useAdminLocale } from '../../composables/useAdminLocale';
import api from '../../api';
import CountryFlagSelect from '../../components/outright/CountryFlagSelect.vue';
import LogoUrlField from '../../components/LogoUrlField.vue';
import {
countryFlagUrl,
getBuiltinCountry,
resolveCountryCode,
type BuiltinCountry,
@@ -30,6 +29,7 @@ interface SelectionRow {
logoUrl: string | null;
editOdds: number;
editCountryCode: string;
editLogoUrl: string;
}
const loading = ref(false);
@@ -39,7 +39,9 @@ const meta = ref({
leagueZh: '',
leagueEn: '',
leagueCode: '',
matchName: '',
titleZh: '',
titleEn: '',
titleMs: '',
status: 'DRAFT',
expectedCanonicalCount: null as number | null,
playerVisible: true,
@@ -53,6 +55,7 @@ const addForm = ref({
teamCode: '',
teamZh: '',
teamEn: '',
logoUrl: '',
odds: 10,
});
@@ -81,7 +84,9 @@ async function load() {
leagueZh: string;
leagueEn: string;
leagueCode: string;
matchName: string;
titleZh: string;
titleEn: string;
titleMs: string;
status: string;
expectedCanonicalCount: number | null;
playerVisible: boolean;
@@ -92,7 +97,9 @@ async function load() {
leagueZh: payload.leagueZh,
leagueEn: payload.leagueEn,
leagueCode: payload.leagueCode,
matchName: payload.matchName,
titleZh: payload.titleZh ?? '',
titleEn: payload.titleEn ?? '',
titleMs: payload.titleMs ?? '',
status: payload.status,
expectedCanonicalCount: payload.expectedCanonicalCount,
playerVisible: payload.playerVisible,
@@ -103,6 +110,7 @@ async function load() {
logoUrl: s.logoUrl ?? null,
editOdds: Number(s.odds),
editCountryCode: resolveCountryCode(s.teamCode, s.logoUrl ?? null),
editLogoUrl: s.logoUrl ?? '',
}));
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
@@ -119,7 +127,9 @@ async function saveMeta() {
try {
await api.put(`/admin/outrights/${matchId.value}`, {
status: meta.value.status,
matchName: meta.value.matchName,
titleZh: meta.value.titleZh,
titleEn: meta.value.titleEn,
titleMs: meta.value.titleMs,
});
ElMessage.success(t('msg.saved'));
await load();
@@ -171,12 +181,12 @@ async function submitAdd() {
teamCode: country.code,
teamZh: country.nameZh,
teamEn: country.nameEn,
logoUrl: countryFlagUrl(country),
logoUrl: addForm.value.logoUrl.trim() || undefined,
odds: addForm.value.odds,
});
ElMessage.success(t('msg.saved'));
addVisible.value = false;
addForm.value = { countryCode: '', teamCode: '', teamZh: '', teamEn: '', odds: 10 };
addForm.value = { countryCode: '', teamCode: '', teamZh: '', teamEn: '', logoUrl: '', odds: 10 };
await load();
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
@@ -255,8 +265,29 @@ function rowDisplayCode(row: SelectionRow) {
function isRowDirty(row: SelectionRow) {
const countryDirty =
row.editCountryCode !== resolveCountryCode(row.teamCode, row.logoUrl);
const logoDirty = (row.editLogoUrl || '').trim() !== (row.logoUrl || '').trim();
const oddsDirty = row.editOdds !== Number(row.odds);
return countryDirty || oddsDirty;
return countryDirty || logoDirty || oddsDirty;
}
function onRowCountryPick(row: SelectionRow, country: BuiltinCountry) {
row.editCountryCode = country.code;
}
function onRowLogoChange(row: SelectionRow, url: string) {
row.editLogoUrl = url;
const code = resolveCountryCode(row.editCountryCode || row.teamCode, url);
if (code) row.editCountryCode = code;
}
function onAddLogoChange(url: string) {
addForm.value.logoUrl = url;
const code = resolveCountryCode(addForm.value.countryCode, url);
if (code) addForm.value.countryCode = code;
}
function onAddLogoPick(country: BuiltinCountry) {
onAddCountryPick(country);
}
async function saveRow(row: SelectionRow) {
@@ -271,8 +302,9 @@ async function saveRow(row: SelectionRow) {
const country = getBuiltinCountry(row.editCountryCode);
const countryDirty =
row.editCountryCode !== resolveCountryCode(row.teamCode, row.logoUrl);
const logoDirty = (row.editLogoUrl || '').trim() !== (row.logoUrl || '').trim();
if (countryDirty) {
if (countryDirty || logoDirty) {
if (!country) {
ElMessage.warning(t('outright.err_country'));
return;
@@ -281,7 +313,7 @@ async function saveRow(row: SelectionRow) {
teamCode: country.code,
teamZh: country.nameZh,
teamEn: country.nameEn,
logoUrl: countryFlagUrl(country),
logoUrl: row.editLogoUrl.trim() || undefined,
});
}
@@ -322,13 +354,19 @@ async function saveRow(row: SelectionRow) {
<section class="panel settings-panel">
<div class="settings-top">
<el-input
v-model="meta.matchName"
size="small"
class="title-input"
:placeholder="t('outright.field.title_placeholder')"
@keyup.enter="saveMeta"
/>
<div class="title-fields">
<el-form label-width="88px" size="small" @submit.prevent="saveMeta">
<el-form-item :label="t('outright.field.title_zh')">
<el-input v-model="meta.titleZh" @keyup.enter="saveMeta" />
</el-form-item>
<el-form-item :label="t('outright.field.title_en')">
<el-input v-model="meta.titleEn" @keyup.enter="saveMeta" />
</el-form-item>
<el-form-item :label="t('outright.field.title_ms')">
<el-input v-model="meta.titleMs" @keyup.enter="saveMeta" />
</el-form-item>
</el-form>
</div>
<div class="settings-actions">
<el-button size="small" :loading="saving" @click="saveMeta">
{{ t('common.save') }}
@@ -369,11 +407,14 @@ async function saveRow(row: SelectionRow) {
<div class="table-wrap">
<el-table :data="selections" stripe size="small" empty-text="">
<el-table-column prop="rank" :label="t('outright.col.rank')" width="72" align="center" />
<el-table-column :label="t('outright.col.country')" min-width="220">
<el-table-column :label="t('outright.col.country')" min-width="340">
<template #default="{ row }">
<CountryFlagSelect
v-model="row.editCountryCode"
:disabled="!!savingRowId"
<LogoUrlField
:model-value="row.editLogoUrl"
compact
:team-code="row.editCountryCode || row.teamCode"
@update:model-value="onRowLogoChange(row, $event)"
@pick="onRowCountryPick(row, $event)"
/>
</template>
</el-table-column>
@@ -431,13 +472,14 @@ async function saveRow(row: SelectionRow) {
</div>
</section>
<el-dialog v-model="addVisible" :title="t('outright.btn.add_team')" width="440px">
<el-dialog v-model="addVisible" :title="t('outright.btn.add_team')" width="520px">
<el-form label-width="100px">
<el-form-item :label="t('outright.col.country')" required>
<CountryFlagSelect
v-model="addForm.countryCode"
size="default"
@pick="onAddCountryPick"
<LogoUrlField
:model-value="addForm.logoUrl"
:team-code="addForm.countryCode"
@update:model-value="onAddLogoChange"
@pick="onAddLogoPick"
/>
</el-form-item>
<el-form-item v-if="addForm.teamCode" :label="t('outright.col.code')">
@@ -505,9 +547,14 @@ async function saveRow(row: SelectionRow) {
.settings-top {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
gap: 12px;
}
.title-fields {
flex: 1;
min-width: 0;
}
.league-meta {
@@ -528,11 +575,6 @@ async function saveRow(row: SelectionRow) {
gap: 6px;
}
.title-input {
flex: 1;
min-width: 0;
}
.panel-head.compact {
display: flex;
align-items: center;

View File

@@ -59,6 +59,7 @@ const createForm = ref({
leagueId: '',
titleZh: '',
titleEn: '',
titleMs: '',
status: 'PUBLISHED',
});
@@ -166,7 +167,7 @@ async function submitCreate() {
const { data } = await api.post('/admin/outrights', createForm.value);
ElMessage.success(t('msg.saved'));
createVisible.value = false;
createForm.value = { leagueId: '', titleZh: '', titleEn: '', status: 'PUBLISHED' };
createForm.value = { leagueId: '', titleZh: '', titleEn: '', titleMs: '', status: 'PUBLISHED' };
listReady.value = false;
rowDetails.value = {};
await loadEvents(false);
@@ -292,7 +293,7 @@ onMounted(() => {
</el-table>
</el-card>
<el-dialog v-model="createVisible" :title="t('outright.btn.create_event')" width="440px">
<el-dialog v-model="createVisible" :title="t('outright.btn.create_event')" width="480px">
<el-form label-width="100px" size="small">
<el-form-item :label="t('outright.field.league')">
<el-select v-model="createForm.leagueId" filterable style="width: 100%">
@@ -310,6 +311,9 @@ onMounted(() => {
<el-form-item :label="t('outright.field.title_en')">
<el-input v-model="createForm.titleEn" />
</el-form-item>
<el-form-item :label="t('outright.field.title_ms')">
<el-input v-model="createForm.titleMs" />
</el-form-item>
</el-form>
<template #footer>
<el-button size="small" @click="createVisible = false">{{ t('common.cancel') }}</el-button>