- 固定48强基准数据、同步种子与后台世界杯夺冠页 - 补全 user_preferences 迁移文件;新增启动指南与默认数据说明 Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
4.7 KiB
Vue
151 lines
4.7 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue';
|
|
import { ElMessage } from 'element-plus';
|
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
|
import api from '../api';
|
|
|
|
const { t } = useAdminLocale();
|
|
|
|
interface SelectionRow {
|
|
id: string;
|
|
teamCode: string;
|
|
rank: number;
|
|
teamZh: string;
|
|
teamEn: string;
|
|
odds: string;
|
|
oddsVersion: string;
|
|
status: string;
|
|
editOdds: number;
|
|
}
|
|
|
|
const loading = ref(false);
|
|
const saving = ref(false);
|
|
const leagueZh = ref('');
|
|
const leagueEn = ref('');
|
|
const selections = ref<SelectionRow[]>([]);
|
|
const expectedCount = ref(48);
|
|
|
|
async function load() {
|
|
loading.value = true;
|
|
try {
|
|
const { data } = await api.get('/admin/outrights/wc2026');
|
|
const payload = data.data as {
|
|
leagueZh: string;
|
|
leagueEn: string;
|
|
selections: SelectionRow[];
|
|
expectedCount: number;
|
|
};
|
|
leagueZh.value = payload.leagueZh;
|
|
leagueEn.value = payload.leagueEn;
|
|
expectedCount.value = payload.expectedCount;
|
|
selections.value = payload.selections.map((s) => ({
|
|
...s,
|
|
editOdds: Number(s.odds),
|
|
}));
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function applyCanonical() {
|
|
loading.value = true;
|
|
try {
|
|
await api.post('/admin/outrights/wc2026/apply-canonical');
|
|
ElMessage.success(t('msg.outright_canonical_applied'));
|
|
await load();
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function saveAll() {
|
|
const invalid = selections.value.find((s) => !s.editOdds || s.editOdds <= 1);
|
|
if (invalid) {
|
|
ElMessage.warning(t('outright.err_odds_min'));
|
|
return;
|
|
}
|
|
saving.value = true;
|
|
try {
|
|
await api.put('/admin/outrights/wc2026/odds', {
|
|
updates: selections.value.map((s) => ({
|
|
selectionId: s.id,
|
|
odds: s.editOdds,
|
|
})),
|
|
});
|
|
ElMessage.success(t('msg.outright_odds_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 {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(load);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="admin-list-page">
|
|
<div class="page-header">
|
|
<h2 class="page-title">{{ t('page.outright.title') }}</h2>
|
|
<span class="page-desc">{{ t('page.outright.desc') }}</span>
|
|
</div>
|
|
|
|
<el-card class="data-card" shadow="never" v-loading="loading">
|
|
<div class="meta-row">
|
|
<span>{{ leagueZh }} / {{ leagueEn }}</span>
|
|
<span class="meta-count">
|
|
{{ t('outright.team_count', { n: selections.length, total: expectedCount }) }}
|
|
</span>
|
|
</div>
|
|
<div class="table-wrap">
|
|
<el-table :data="selections" stripe>
|
|
<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.odds')" width="160" align="right">
|
|
<template #default="{ row }">
|
|
<el-input-number
|
|
v-model="row.editOdds"
|
|
:min="1.01"
|
|
:step="0.05"
|
|
:precision="2"
|
|
controls-position="right"
|
|
size="small"
|
|
style="width: 130px"
|
|
/>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
<div class="footer-actions">
|
|
<el-button @click="load">{{ t('common.reset') }}</el-button>
|
|
<el-button :loading="loading" @click="applyCanonical">
|
|
{{ t('outright.btn.apply_canonical') }}
|
|
</el-button>
|
|
<el-button type="primary" :loading="saving" @click="saveAll">
|
|
{{ t('outright.btn.save_odds') }}
|
|
</el-button>
|
|
</div>
|
|
</el-card>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.page-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; }
|
|
.page-title { font-size: 20px; font-weight: 700; color: #e0e0e0; }
|
|
.page-desc { font-size: 13px; color: #888; }
|
|
.data-card { border-radius: 12px; }
|
|
.meta-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 13px; color: #aaa; }
|
|
.meta-count { color: #666; }
|
|
.footer-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; }
|
|
</style>
|