feat: WC2026 赛事 seed、生产上线初始化脚本与目录归档

重构 seed 为 WC2026 72 场小组赛与 48 强优胜盘;新增 production 模式仅保留 admin 与赛事示例;提供 prod-init-db 全量重置脚本;管理端 i18n 分包与赛事归档能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 18:17:00 +08:00
parent 8f14e85ebd
commit e7e938f261
94 changed files with 12332 additions and 976 deletions

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useAdminLocale } from '../composables/useAdminLocale';
import api from '../api';
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
@@ -86,8 +87,33 @@ function openScreenshot(url: string) {
previewVisible.value = true;
}
async function confirmDepositAction(message: string, title: string): Promise<boolean> {
try {
await ElMessageBox.confirm(message, title, {
type: 'warning',
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
});
return true;
} catch {
return false;
}
}
function showApiError(e: unknown) {
const msg = (e as { response?: { data?: { message?: string } } })?.response?.data?.message;
ElMessage.error(msg || 'Error');
}
async function handleApprove() {
if (!approveTarget.value) return;
const amountText = formatAmount(String(approveAmount.value));
if (!await confirmDepositAction(
t('deposit.confirm_approve_message', { amount: amountText }),
t('deposit.confirm_approve'),
)) {
return;
}
try {
await api.post(`/admin/deposit-orders/${approveTarget.value.id}/approve`, {
approvedAmount: approveAmount.value,
@@ -95,15 +121,15 @@ async function handleApprove() {
});
approveDialogVisible.value = false;
await fetchList();
} catch (e: any) {
alert(e.response?.data?.message || 'Error');
} catch (e: unknown) {
showApiError(e);
}
}
async function handleReject() {
if (!rejectTarget.value) return;
if (!rejectReason.value.trim()) {
alert(t('deposit.reason_required'));
ElMessage.warning(t('deposit.reason_required'));
return;
}
try {
@@ -112,8 +138,45 @@ async function handleReject() {
});
rejectDialogVisible.value = false;
await fetchList();
} catch (e: any) {
alert(e.response?.data?.message || 'Error');
} catch (e: unknown) {
showApiError(e);
}
}
const DEPOSIT_REVOKE_WINDOW_MS = 5 * 60 * 1000;
function canRevokeApproved(row: DepositOrderRow): boolean {
if (row.status !== 'APPROVED' || !row.reviewedAt) return false;
return Date.now() - new Date(row.reviewedAt).getTime() <= DEPOSIT_REVOKE_WINDOW_MS;
}
async function handleReopen(row: DepositOrderRow) {
if (!await confirmDepositAction(t('deposit.confirm_reopen'), t('deposit.reopen_review'))) return;
try {
await api.post(`/admin/deposit-orders/${row.id}/reopen`);
await fetchList();
} catch (e: unknown) {
showApiError(e);
}
}
async function handleRevoke(row: DepositOrderRow) {
if (!await confirmDepositAction(t('deposit.confirm_revoke'), t('deposit.revoke'))) return;
try {
await api.post(`/admin/deposit-orders/${row.id}/reopen`);
await fetchList();
} catch (e: unknown) {
showApiError(e);
}
}
async function handleDelete(row: DepositOrderRow) {
if (!await confirmDepositAction(t('deposit.confirm_delete'), t('deposit.delete'))) return;
try {
await api.delete(`/admin/deposit-orders/${row.id}`);
await fetchList();
} catch (e: unknown) {
showApiError(e);
}
}
@@ -202,6 +265,21 @@ onMounted(fetchList);
<template v-if="row.status === 'PENDING'">
<button class="btn-sm btn-approve" @click="openApprove(row)">{{ t('deposit.approve') }}</button>
<button class="btn-sm btn-reject" @click="openReject(row)">{{ t('deposit.reject') }}</button>
<button class="btn-sm btn-delete" @click="handleDelete(row)">{{ t('deposit.delete') }}</button>
</template>
<template v-else-if="row.status === 'APPROVED'">
<button
v-if="canRevokeApproved(row)"
class="btn-sm btn-reopen"
@click="handleRevoke(row)"
>
{{ t('deposit.revoke') }}
</button>
<button class="btn-sm btn-delete" @click="handleDelete(row)">{{ t('deposit.delete') }}</button>
</template>
<template v-else-if="row.status === 'REJECTED'">
<button class="btn-sm btn-reopen" @click="handleReopen(row)">{{ t('deposit.reopen_review') }}</button>
<button class="btn-sm btn-delete" @click="handleDelete(row)">{{ t('deposit.delete') }}</button>
</template>
</td>
</tr>
@@ -218,6 +296,7 @@ onMounted(fetchList);
<div v-if="approveDialogVisible" class="dialog-overlay" @click.self="approveDialogVisible = false">
<div class="dialog-box">
<h3>{{ t('deposit.approve_title') }}</h3>
<p class="approve-check-hint">{{ t('deposit.approve_check_hint') }}</p>
<div v-if="approveTarget" class="approve-content">
<div class="info-row">
<span>{{ t('deposit.player') }}:</span> <strong>{{ approveTarget.playerUsername }}</strong>
@@ -305,6 +384,11 @@ onMounted(fetchList);
.btn-approve:hover { background: rgba(61, 115, 88, 0.24); }
.btn-reject { background: #3a1a1a; color: #f56c6c; }
.btn-reject:hover { background: #4a2525; }
.btn-reopen { background: #2a2410; color: #e6a23c; }
.btn-reopen:hover { background: #3a3218; }
.btn-delete { background: #2a2a2a; color: #aaa; }
.btn-delete:hover { background: #3a3a3a; color: #f56c6c; }
.actions-cell { white-space: nowrap; }
.pagination { display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 16px; }
.pagination button { background: #333; color: #ddd; border: none; border-radius: 4px; padding: 6px 14px; cursor: pointer; }
.pagination button:disabled { opacity: 0.4; cursor: default; }
@@ -312,6 +396,16 @@ onMounted(fetchList);
.dialog-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 999; }
.dialog-box { background: #1e1e1e; border-radius: 8px; padding: 24px; min-width: 440px; max-width: 560px; }
.dialog-box h3 { margin: 0 0 16px; font-size: 16px; }
.approve-check-hint {
margin: -8px 0 14px;
padding: 10px 12px;
border-radius: 6px;
background: rgba(230, 162, 60, 0.12);
border: 1px solid rgba(230, 162, 60, 0.35);
color: #e6a23c;
font-size: 13px;
line-height: 1.5;
}
.info-row { margin-bottom: 8px; font-size: 13px; }
.info-row span { color: #888; margin-right: 4px; }
.approve-screenshot { width: 200px; max-height: 200px; object-fit: contain; border-radius: 4px; cursor: pointer; margin-top: 4px; }