389 lines
11 KiB
Vue
389 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { ElMessage } from 'element-plus';
|
|
import { useAdminLocale } from '../../composables/useAdminLocale';
|
|
import api from '../../api';
|
|
|
|
interface OutrightEventItem {
|
|
id: string;
|
|
leagueId: string;
|
|
leagueCode: string;
|
|
leagueZh: string;
|
|
leagueEn: string;
|
|
matchName: string;
|
|
status: string;
|
|
selectionCount: number;
|
|
canImportCanonical: boolean;
|
|
playerVisible: boolean;
|
|
playerHiddenReason: string | null;
|
|
}
|
|
|
|
interface SelectionPreview {
|
|
rank: number;
|
|
teamZh: string;
|
|
teamEn: string;
|
|
teamCode: string;
|
|
odds: string;
|
|
}
|
|
|
|
interface RowDetail {
|
|
leagueZh: string;
|
|
leagueEn: string;
|
|
matchName: string;
|
|
playerVisible: boolean;
|
|
playerHiddenReason: string | null;
|
|
selections: SelectionPreview[];
|
|
}
|
|
|
|
interface LeagueOption {
|
|
id: string;
|
|
code: string;
|
|
nameZh: string;
|
|
nameEn: string;
|
|
}
|
|
|
|
const router = useRouter();
|
|
const { t } = useAdminLocale();
|
|
|
|
const listReady = ref(false);
|
|
const listLoading = ref(false);
|
|
const importing = ref(false);
|
|
const events = ref<OutrightEventItem[]>([]);
|
|
const leagues = ref<LeagueOption[]>([]);
|
|
const expandLoadingId = ref<string | null>(null);
|
|
const rowDetails = ref<Record<string, RowDetail>>({});
|
|
const createVisible = ref(false);
|
|
const createLoading = ref(false);
|
|
const createForm = ref({
|
|
leagueId: '',
|
|
titleZh: '',
|
|
titleEn: '',
|
|
titleMs: '',
|
|
status: 'PUBLISHED',
|
|
});
|
|
|
|
function eventTitle(ev: OutrightEventItem) {
|
|
return ev.matchName || ev.leagueZh || ev.leagueEn || '—';
|
|
}
|
|
|
|
function leagueLabel(ev: OutrightEventItem) {
|
|
const name = ev.leagueZh || ev.leagueEn;
|
|
return name ? `${name} (${ev.leagueCode})` : ev.leagueCode;
|
|
}
|
|
|
|
function hiddenTip(reason: string | null) {
|
|
if (!reason) return '';
|
|
return t(`outright.hidden_reason.${reason}`);
|
|
}
|
|
|
|
function goEdit(id: string) {
|
|
router.push({ name: 'admin-outright-edit', params: { matchId: id } });
|
|
}
|
|
|
|
async function loadEvents(silent = false) {
|
|
if (!silent) listLoading.value = true;
|
|
try {
|
|
const { data } = await api.get('/admin/outrights');
|
|
events.value = data.data ?? [];
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
|
} finally {
|
|
listReady.value = true;
|
|
listLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function loadRowDetail(row: OutrightEventItem) {
|
|
if (rowDetails.value[row.id]) return;
|
|
expandLoadingId.value = row.id;
|
|
try {
|
|
const { data } = await api.get(`/admin/outrights/${row.id}`);
|
|
const payload = data.data as {
|
|
leagueZh: string;
|
|
leagueEn: string;
|
|
matchName: string;
|
|
playerVisible: boolean;
|
|
playerHiddenReason: string | null;
|
|
selections: SelectionPreview[];
|
|
};
|
|
rowDetails.value[row.id] = {
|
|
leagueZh: payload.leagueZh,
|
|
leagueEn: payload.leagueEn,
|
|
matchName: payload.matchName,
|
|
playerVisible: payload.playerVisible,
|
|
playerHiddenReason: payload.playerHiddenReason,
|
|
selections: payload.selections ?? [],
|
|
};
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
|
} finally {
|
|
expandLoadingId.value = null;
|
|
}
|
|
}
|
|
|
|
async function onExpandChange(row: OutrightEventItem, expanded: OutrightEventItem[]) {
|
|
const opened = expanded.some((r) => r.id === row.id);
|
|
if (!opened) return;
|
|
await loadRowDetail(row);
|
|
}
|
|
|
|
async function loadLeagues() {
|
|
try {
|
|
const { data } = await api.get('/admin/outrights/leagues');
|
|
leagues.value = data.data ?? [];
|
|
} catch {
|
|
leagues.value = [];
|
|
}
|
|
}
|
|
|
|
async function importWc2026() {
|
|
importing.value = true;
|
|
try {
|
|
const { data } = await api.post('/admin/outrights/import/wc2026');
|
|
ElMessage.success(t('msg.outright_canonical_applied'));
|
|
listReady.value = false;
|
|
rowDetails.value = {};
|
|
await loadEvents(false);
|
|
const id = data.data?.id as string | undefined;
|
|
if (id) goEdit(id);
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
|
} finally {
|
|
importing.value = false;
|
|
}
|
|
}
|
|
|
|
async function submitCreate() {
|
|
if (!createForm.value.leagueId) {
|
|
ElMessage.warning(t('outright.err_league'));
|
|
return;
|
|
}
|
|
createLoading.value = true;
|
|
try {
|
|
const { data } = await api.post('/admin/outrights', createForm.value);
|
|
ElMessage.success(t('msg.saved'));
|
|
createVisible.value = false;
|
|
createForm.value = { leagueId: '', titleZh: '', titleEn: '', titleMs: '', status: 'PUBLISHED' };
|
|
listReady.value = false;
|
|
rowDetails.value = {};
|
|
await loadEvents(false);
|
|
const id = data.data?.id as string;
|
|
if (id) goEdit(id);
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
|
} finally {
|
|
createLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function refreshList() {
|
|
listReady.value = false;
|
|
rowDetails.value = {};
|
|
void loadEvents(false);
|
|
}
|
|
|
|
onMounted(() => {
|
|
void loadLeagues();
|
|
void loadEvents(true);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="outright-list-page admin-list-page">
|
|
<div class="page-toolbar">
|
|
<el-button size="small" :loading="importing" @click="importWc2026">
|
|
{{ t('outright.btn.import_wc2026') }}
|
|
</el-button>
|
|
<el-button size="small" :loading="listLoading" @click="refreshList">
|
|
{{ t('common.reset') }}
|
|
</el-button>
|
|
<el-button type="primary" size="small" @click="createVisible = true">
|
|
{{ t('outright.btn.create_event') }}
|
|
</el-button>
|
|
</div>
|
|
|
|
<el-card class="list-card" shadow="never">
|
|
<el-table
|
|
:data="events"
|
|
row-key="id"
|
|
size="small"
|
|
:empty-text="listReady ? '—' : ''"
|
|
@expand-change="onExpandChange"
|
|
>
|
|
<el-table-column type="expand" width="44">
|
|
<template #default="{ row }">
|
|
<div v-loading="expandLoadingId === row.id" class="expand-body">
|
|
<template v-if="rowDetails[row.id]">
|
|
<p v-if="rowDetails[row.id].leagueEn" class="expand-line">
|
|
<span class="expand-k">{{ t('outright.col.league_en') }}</span>
|
|
{{ rowDetails[row.id].leagueEn }}
|
|
</p>
|
|
<p class="expand-line">
|
|
<span class="expand-k">ID</span>
|
|
{{ row.id }}
|
|
</p>
|
|
<p v-if="!rowDetails[row.id].playerVisible" class="expand-warn">
|
|
{{ hiddenTip(rowDetails[row.id].playerHiddenReason) }}
|
|
</p>
|
|
<el-table
|
|
v-if="rowDetails[row.id].selections.length"
|
|
:data="rowDetails[row.id].selections"
|
|
size="small"
|
|
class="preview-table"
|
|
max-height="240"
|
|
>
|
|
<el-table-column prop="rank" :label="t('outright.col.rank')" width="56" />
|
|
<el-table-column prop="teamZh" :label="t('outright.col.team_zh')" min-width="100" />
|
|
<el-table-column prop="teamCode" :label="t('outright.col.code')" width="72" />
|
|
<el-table-column prop="odds" :label="t('outright.col.odds')" width="88" align="right" />
|
|
</el-table>
|
|
<p v-else class="expand-empty">{{ t('outright.expand_no_teams') }}</p>
|
|
<div class="expand-actions">
|
|
<el-button type="primary" size="small" @click="goEdit(row.id)">
|
|
{{ t('common.edit') }}
|
|
</el-button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('outright.field.title')" min-width="160">
|
|
<template #default="{ row }">{{ eventTitle(row) }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('outright.field.league')" min-width="180">
|
|
<template #default="{ row }">{{ leagueLabel(row) }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('outright.field.status')" width="96" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag
|
|
size="small"
|
|
:type="row.status === 'PUBLISHED' ? 'success' : 'info'"
|
|
effect="dark"
|
|
>
|
|
{{
|
|
row.status === 'PUBLISHED'
|
|
? t('outright.status.published')
|
|
: t('outright.status.draft')
|
|
}}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('outright.col.teams')" width="88" align="center">
|
|
<template #default="{ row }">{{ row.selectionCount }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('outright.col.player_visible')" width="108" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag size="small" :type="row.playerVisible ? 'success' : 'warning'" effect="plain">
|
|
{{ row.playerVisible ? t('common.yes') : t('common.no') }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('common.actions')" width="88" align="center" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button type="primary" link @click="goEdit(row.id)">
|
|
{{ t('common.edit') }}
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
|
|
<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%">
|
|
<el-option
|
|
v-for="lg in leagues"
|
|
:key="lg.id"
|
|
:value="lg.id"
|
|
:label="`${lg.nameZh || lg.code} (${lg.code})`"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item :label="t('outright.field.title_zh')">
|
|
<el-input v-model="createForm.titleZh" />
|
|
</el-form-item>
|
|
<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>
|
|
<el-button type="primary" size="small" :loading="createLoading" @click="submitCreate">
|
|
{{ t('common.confirm') }}
|
|
</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.outright-list-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.list-card {
|
|
flex: 1;
|
|
min-height: 0;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.list-card :deep(.el-card__body) {
|
|
padding: 0;
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.expand-body {
|
|
padding: 10px 12px 12px 20px;
|
|
min-height: 48px;
|
|
}
|
|
|
|
.expand-line {
|
|
margin: 0 0 4px;
|
|
font-size: 12px;
|
|
color: #999;
|
|
}
|
|
|
|
.expand-k {
|
|
display: inline-block;
|
|
min-width: 72px;
|
|
color: #666;
|
|
margin-right: 6px;
|
|
}
|
|
|
|
.expand-warn {
|
|
margin: 0 0 8px;
|
|
font-size: 12px;
|
|
color: #c9a227;
|
|
}
|
|
|
|
.expand-empty {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
|
|
.preview-table {
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.expand-actions {
|
|
margin-top: 10px;
|
|
}
|
|
</style>
|