feat(admin,player,api): 公共管理与优胜冠军国旗、玩家端内容对接
新增公共内容 CRUD 与批量操作;公告滚动合并管理;优胜冠军内置国家选择与单行保存;玩家端统一 usePlayerHome 对接轮播与跑马灯。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,13 @@ 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 {
|
||||
countryFlagUrl,
|
||||
getBuiltinCountry,
|
||||
resolveCountryCode,
|
||||
type BuiltinCountry,
|
||||
} from '../../data/builtinCountries';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -20,11 +27,14 @@ interface SelectionRow {
|
||||
odds: string;
|
||||
oddsVersion: string;
|
||||
status: string;
|
||||
logoUrl: string | null;
|
||||
editOdds: number;
|
||||
editCountryCode: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const savingRowId = ref<string | null>(null);
|
||||
const meta = ref({
|
||||
leagueZh: '',
|
||||
leagueEn: '',
|
||||
@@ -39,12 +49,29 @@ const selections = ref<SelectionRow[]>([]);
|
||||
|
||||
const addVisible = ref(false);
|
||||
const addForm = ref({
|
||||
countryCode: '',
|
||||
teamCode: '',
|
||||
teamZh: '',
|
||||
teamEn: '',
|
||||
odds: 10,
|
||||
});
|
||||
|
||||
function applyCountry(target: {
|
||||
countryCode: string;
|
||||
teamCode: string;
|
||||
teamZh: string;
|
||||
teamEn: string;
|
||||
}, country: BuiltinCountry) {
|
||||
target.countryCode = country.code;
|
||||
target.teamCode = country.code;
|
||||
target.teamZh = country.nameZh;
|
||||
target.teamEn = country.nameEn;
|
||||
}
|
||||
|
||||
function onAddCountryPick(country: BuiltinCountry) {
|
||||
applyCountry(addForm.value, country);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!matchId.value) return;
|
||||
loading.value = true;
|
||||
@@ -59,7 +86,7 @@ async function load() {
|
||||
expectedCanonicalCount: number | null;
|
||||
playerVisible: boolean;
|
||||
playerHiddenReason: string | null;
|
||||
selections: SelectionRow[];
|
||||
selections: Array<SelectionRow & { logoUrl?: string | null }>;
|
||||
};
|
||||
meta.value = {
|
||||
leagueZh: payload.leagueZh,
|
||||
@@ -73,7 +100,9 @@ async function load() {
|
||||
};
|
||||
selections.value = payload.selections.map((s) => ({
|
||||
...s,
|
||||
logoUrl: s.logoUrl ?? null,
|
||||
editOdds: Number(s.odds),
|
||||
editCountryCode: resolveCountryCode(s.teamCode, s.logoUrl ?? null),
|
||||
}));
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
@@ -127,21 +156,27 @@ async function saveAllOdds() {
|
||||
}
|
||||
|
||||
async function submitAdd() {
|
||||
if (!addForm.value.teamCode.trim()) {
|
||||
ElMessage.warning(t('outright.err_team_code'));
|
||||
if (!addForm.value.countryCode) {
|
||||
ElMessage.warning(t('outright.err_country'));
|
||||
return;
|
||||
}
|
||||
const country = getBuiltinCountry(addForm.value.countryCode);
|
||||
if (!country) {
|
||||
ElMessage.warning(t('outright.err_country'));
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await api.post(`/admin/outrights/${matchId.value}/selections`, {
|
||||
teamCode: addForm.value.teamCode.trim().toUpperCase(),
|
||||
teamZh: addForm.value.teamZh,
|
||||
teamEn: addForm.value.teamEn,
|
||||
teamCode: country.code,
|
||||
teamZh: country.nameZh,
|
||||
teamEn: country.nameEn,
|
||||
logoUrl: countryFlagUrl(country),
|
||||
odds: addForm.value.odds,
|
||||
});
|
||||
ElMessage.success(t('msg.saved'));
|
||||
addVisible.value = false;
|
||||
addForm.value = { teamCode: '', teamZh: '', teamEn: '', odds: 10 };
|
||||
addForm.value = { countryCode: '', teamCode: '', teamZh: '', teamEn: '', odds: 10 };
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
@@ -200,6 +235,71 @@ async function saveMetaWithStatus(status: 'DRAFT' | 'PUBLISHED') {
|
||||
meta.value.status = status;
|
||||
await saveMeta();
|
||||
}
|
||||
|
||||
function rowDisplayName(row: SelectionRow, field: 'zh' | 'en') {
|
||||
const picked = getBuiltinCountry(row.editCountryCode);
|
||||
if (picked && row.editCountryCode !== resolveCountryCode(row.teamCode, row.logoUrl)) {
|
||||
return field === 'zh' ? picked.nameZh : picked.nameEn;
|
||||
}
|
||||
return field === 'zh' ? row.teamZh : row.teamEn;
|
||||
}
|
||||
|
||||
function rowDisplayCode(row: SelectionRow) {
|
||||
const picked = getBuiltinCountry(row.editCountryCode);
|
||||
if (picked && row.editCountryCode !== resolveCountryCode(row.teamCode, row.logoUrl)) {
|
||||
return picked.code;
|
||||
}
|
||||
return row.teamCode;
|
||||
}
|
||||
|
||||
function isRowDirty(row: SelectionRow) {
|
||||
const countryDirty =
|
||||
row.editCountryCode !== resolveCountryCode(row.teamCode, row.logoUrl);
|
||||
const oddsDirty = row.editOdds !== Number(row.odds);
|
||||
return countryDirty || oddsDirty;
|
||||
}
|
||||
|
||||
async function saveRow(row: SelectionRow) {
|
||||
if (!isRowDirty(row)) return;
|
||||
if (!row.editOdds || row.editOdds <= 1) {
|
||||
ElMessage.warning(t('outright.err_odds_min'));
|
||||
return;
|
||||
}
|
||||
|
||||
savingRowId.value = row.id;
|
||||
try {
|
||||
const country = getBuiltinCountry(row.editCountryCode);
|
||||
const countryDirty =
|
||||
row.editCountryCode !== resolveCountryCode(row.teamCode, row.logoUrl);
|
||||
|
||||
if (countryDirty) {
|
||||
if (!country) {
|
||||
ElMessage.warning(t('outright.err_country'));
|
||||
return;
|
||||
}
|
||||
await api.patch(`/admin/outrights/${matchId.value}/selections/${row.id}`, {
|
||||
teamCode: country.code,
|
||||
teamZh: country.nameZh,
|
||||
teamEn: country.nameEn,
|
||||
logoUrl: countryFlagUrl(country),
|
||||
});
|
||||
}
|
||||
|
||||
if (row.editOdds !== Number(row.odds)) {
|
||||
await api.put(`/admin/outrights/${matchId.value}/odds`, {
|
||||
updates: [{ selectionId: row.id, odds: row.editOdds }],
|
||||
});
|
||||
}
|
||||
|
||||
ElMessage.success(t('msg.saved'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
savingRowId.value = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -269,9 +369,23 @@ async function saveMetaWithStatus(status: 'DRAFT' | 'PUBLISHED') {
|
||||
<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 prop="teamZh" :label="t('outright.col.team_zh')" min-width="120" />
|
||||
<el-table-column prop="teamEn" :label="t('outright.col.team_en')" min-width="140" />
|
||||
<el-table-column prop="teamCode" :label="t('outright.col.code')" width="88" />
|
||||
<el-table-column :label="t('outright.col.country')" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<CountryFlagSelect
|
||||
v-model="row.editCountryCode"
|
||||
:disabled="!!savingRowId"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('outright.col.team_zh')" min-width="120">
|
||||
<template #default="{ row }">{{ rowDisplayName(row, 'zh') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('outright.col.team_en')" min-width="140">
|
||||
<template #default="{ row }">{{ rowDisplayName(row, 'en') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('outright.col.code')" width="88">
|
||||
<template #default="{ row }">{{ rowDisplayCode(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('outright.col.odds')" width="160" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
@@ -285,9 +399,23 @@ async function saveMetaWithStatus(status: 'DRAFT' | 'PUBLISHED') {
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.actions')" width="88" align="center">
|
||||
<el-table-column :label="t('common.actions')" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" @click="removeSelection(row)">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:loading="savingRowId === row.id"
|
||||
:disabled="!isRowDirty(row) || (!!savingRowId && savingRowId !== row.id)"
|
||||
@click="saveRow(row)"
|
||||
>
|
||||
{{ t('common.save') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="!!savingRowId"
|
||||
@click="removeSelection(row)"
|
||||
>
|
||||
{{ t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -303,16 +431,23 @@ async function saveMetaWithStatus(status: 'DRAFT' | 'PUBLISHED') {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="addVisible" :title="t('outright.btn.add_team')" width="420px">
|
||||
<el-dialog v-model="addVisible" :title="t('outright.btn.add_team')" width="440px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item :label="t('outright.col.code')">
|
||||
<el-input v-model="addForm.teamCode" placeholder="FRA" />
|
||||
<el-form-item :label="t('outright.col.country')" required>
|
||||
<CountryFlagSelect
|
||||
v-model="addForm.countryCode"
|
||||
size="default"
|
||||
@pick="onAddCountryPick"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('outright.col.team_zh')">
|
||||
<el-input v-model="addForm.teamZh" />
|
||||
<el-form-item v-if="addForm.teamCode" :label="t('outright.col.code')">
|
||||
<span class="readonly-field">{{ addForm.teamCode }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('outright.col.team_en')">
|
||||
<el-input v-model="addForm.teamEn" />
|
||||
<el-form-item v-if="addForm.teamZh" :label="t('outright.col.team_zh')">
|
||||
<span class="readonly-field">{{ addForm.teamZh }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="addForm.teamEn" :label="t('outright.col.team_en')">
|
||||
<span class="readonly-field">{{ addForm.teamEn }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('outright.col.odds')">
|
||||
<el-input-number v-model="addForm.odds" :min="1.01" :step="0.05" :precision="2" />
|
||||
@@ -430,4 +565,9 @@ async function saveMetaWithStatus(status: 'DRAFT' | 'PUBLISHED') {
|
||||
min-height: 80px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user