feat: 世界杯48强夺冠盘、管理端调赔与项目文档
- 固定48强基准数据、同步种子与后台世界杯夺冠页 - 补全 user_preferences 迁移文件;新增启动指南与默认数据说明 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -33,6 +33,7 @@ const zh: Record<string, string> = {
|
||||
'nav.users': '玩家管理',
|
||||
'nav.agents': '代理管理',
|
||||
'nav.matches': '赛事管理',
|
||||
'nav.outright': '世界杯夺冠',
|
||||
'nav.bets': '注单管理',
|
||||
'nav.cashback': '返水管理',
|
||||
'nav.audit': '操作日志',
|
||||
@@ -181,6 +182,7 @@ const en: Record<string, string> = {
|
||||
'nav.users': 'Players',
|
||||
'nav.agents': 'Agents',
|
||||
'nav.matches': 'Matches',
|
||||
'nav.outright': 'WC Winner',
|
||||
'nav.bets': 'Bets',
|
||||
'nav.cashback': 'Cashback',
|
||||
'nav.audit': 'Audit Log',
|
||||
@@ -329,6 +331,7 @@ const ms: Record<string, string> = {
|
||||
'nav.users': 'Pemain',
|
||||
'nav.agents': 'Ejen',
|
||||
'nav.matches': 'Perlawanan',
|
||||
'nav.outright': 'Juara Piala Dunia',
|
||||
'nav.bets': 'Pertaruhan',
|
||||
'nav.cashback': 'Rebat',
|
||||
'nav.audit': 'Log audit',
|
||||
|
||||
@@ -246,6 +246,21 @@ export const adminPagesMs: Record<string, string> = {
|
||||
'msg.credit_adjusted': 'Kredit dikemas kini',
|
||||
'msg.credit_adjust_failed': 'Pelarasan gagal',
|
||||
'msg.outright_no_edit': 'Outright tidak boleh diedit di sini',
|
||||
'msg.outright_odds_saved': 'Odds juara disimpan',
|
||||
'msg.load_failed': 'Gagal memuatkan',
|
||||
|
||||
'page.outright.title': 'Juara Piala Dunia 48',
|
||||
'page.outright.desc': '48 pasukan tetap; laraskan odds juara',
|
||||
'outright.col.rank': 'Kedudukan',
|
||||
'outright.col.team_zh': 'Pasukan (ZH)',
|
||||
'outright.col.team_en': 'Pasukan (EN)',
|
||||
'outright.col.code': 'Kod',
|
||||
'outright.col.odds': 'Odds juara',
|
||||
'outright.btn.save_odds': 'Simpan semua odds',
|
||||
'outright.btn.apply_canonical': 'Guna data jadual asas',
|
||||
'msg.outright_canonical_applied': 'Odds 48 pasukan telah dikemas kini',
|
||||
'outright.team_count': '{n} / {total} pasukan',
|
||||
'outright.err_odds_min': 'Odds mesti lebih 1.00',
|
||||
'msg.load_matches_failed': 'Gagal memuatkan perlawanan',
|
||||
'msg.cashback_issued': 'Rebat telah dikeluarkan',
|
||||
'msg.freeze_confirm_title': '{action} akaun',
|
||||
|
||||
@@ -246,6 +246,21 @@ export const adminPagesZh: Record<string, string> = {
|
||||
'msg.credit_adjusted': '授信已调整',
|
||||
'msg.credit_adjust_failed': '调整失败',
|
||||
'msg.outright_no_edit': '冠军盘不支持在此编辑',
|
||||
'msg.outright_odds_saved': '夺冠赔率已保存',
|
||||
'msg.load_failed': '加载失败',
|
||||
|
||||
'page.outright.title': '世界杯 48 强夺冠',
|
||||
'page.outright.desc': '固定 48 支球队,可调整夺冠赔率(玩家端「优胜冠军」)',
|
||||
'outright.col.rank': '排名',
|
||||
'outright.col.team_zh': '队伍(中文)',
|
||||
'outright.col.team_en': '队伍(英文)',
|
||||
'outright.col.code': '代码',
|
||||
'outright.col.odds': '夺冠赔率',
|
||||
'outright.btn.save_odds': '保存全部赔率',
|
||||
'outright.btn.apply_canonical': '应用表格基准数据',
|
||||
'msg.outright_canonical_applied': '已按基准表写入 48 强夺冠赔率',
|
||||
'outright.team_count': '已配置 {n} / {total} 队',
|
||||
'outright.err_odds_min': '赔率须大于 1.00',
|
||||
'msg.load_matches_failed': '加载赛事失败',
|
||||
'msg.cashback_issued': '返水已发放',
|
||||
'msg.freeze_confirm_title': '{action}账号',
|
||||
@@ -502,6 +517,21 @@ export const adminPagesEn: Record<string, string> = {
|
||||
'msg.credit_adjusted': 'Credit updated',
|
||||
'msg.credit_adjust_failed': 'Adjustment failed',
|
||||
'msg.outright_no_edit': 'Outright cannot be edited here',
|
||||
'msg.outright_odds_saved': 'Outright odds saved',
|
||||
'msg.load_failed': 'Load failed',
|
||||
|
||||
'page.outright.title': 'World Cup 48 — Winner',
|
||||
'page.outright.desc': 'Fixed 48 teams; adjust winner odds (player Outright tab)',
|
||||
'outright.col.rank': 'Rank',
|
||||
'outright.col.team_zh': 'Team (ZH)',
|
||||
'outright.col.team_en': 'Team (EN)',
|
||||
'outright.col.code': 'Code',
|
||||
'outright.col.odds': 'Winner odds',
|
||||
'outright.btn.save_odds': 'Save all odds',
|
||||
'outright.btn.apply_canonical': 'Apply baseline table',
|
||||
'msg.outright_canonical_applied': '48-team winner odds applied from baseline',
|
||||
'outright.team_count': '{n} / {total} teams',
|
||||
'outright.err_odds_min': 'Odds must be greater than 1.00',
|
||||
'msg.load_matches_failed': 'Failed to load matches',
|
||||
'msg.cashback_issued': 'Cashback issued',
|
||||
'msg.freeze_confirm_title': '{action} account',
|
||||
|
||||
@@ -15,6 +15,7 @@ const adminMenus = computed(() => [
|
||||
{ path: '/users', label: t('nav.users') },
|
||||
{ path: '/agents', label: t('nav.agents') },
|
||||
{ path: '/matches', label: t('nav.matches') },
|
||||
{ path: '/world-cup-outright', label: t('nav.outright') },
|
||||
{ path: '/bets', label: t('nav.bets') },
|
||||
{ path: '/cashback', label: t('nav.cashback') },
|
||||
{ path: '/audit', label: t('nav.audit') },
|
||||
|
||||
@@ -26,6 +26,11 @@ const router = createRouter({
|
||||
component: () => import('../views/Matches.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'world-cup-outright',
|
||||
component: () => import('../views/WorldCupOutright.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'bets',
|
||||
component: () => import('../views/Bets.vue'),
|
||||
|
||||
150
apps/admin/src/views/WorldCupOutright.vue
Normal file
150
apps/admin/src/views/WorldCupOutright.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user