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:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, reactive } from 'vue';
|
||||
import { ref, computed, onMounted, watch, reactive, h } from 'vue';
|
||||
import { useAdminLocale } from '../../composables/useAdminLocale';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import api from '../../api';
|
||||
@@ -181,10 +181,15 @@ function ensureSubAgentState(level: number): SubAgentLevelState {
|
||||
}
|
||||
|
||||
/* ─── Agent row expansion (direct players) ─── */
|
||||
const MAX_EXPANDED_AGENT_ROWS = 2;
|
||||
const expandedSet = ref(new Set<string>());
|
||||
const expandedRowKeys = computed(() => Array.from(expandedSet.value));
|
||||
const agentPlayersMap = ref<Record<string, ScopedPlayerRow[]>>({});
|
||||
const agentPlayersMeta = ref<
|
||||
Record<string, { total: number; page: number; pageSize: number }>
|
||||
>({});
|
||||
const expandLoading = ref<Record<string, boolean>>({});
|
||||
const EXPAND_PLAYER_PAGE_SIZE = 20;
|
||||
|
||||
const createVisible = ref(false);
|
||||
const createLoading = ref(false);
|
||||
@@ -439,7 +444,8 @@ function openPlayerWalletLedger(
|
||||
|
||||
/* ─── Agent row expansion ─── */
|
||||
async function onExpandChange(row: AgentSubAgentRow, expandedRows: AgentSubAgentRow[]) {
|
||||
expandedSet.value = new Set(expandedRows.map((r) => r.userId));
|
||||
const ids = expandedRows.map((r) => r.userId).slice(0, MAX_EXPANDED_AGENT_ROWS);
|
||||
expandedSet.value = new Set(ids);
|
||||
if (expandedSet.value.has(row.userId) && !agentPlayersMap.value[row.userId]) {
|
||||
await loadExpansionData(row.userId);
|
||||
}
|
||||
@@ -452,19 +458,41 @@ function onSubAgentRowClick(row: AgentSubAgentRow, _column: unknown, event: Mous
|
||||
if (next.has(userId)) {
|
||||
next.delete(userId);
|
||||
} else {
|
||||
if (next.size >= MAX_EXPANDED_AGENT_ROWS) {
|
||||
const [first] = next;
|
||||
if (first) next.delete(first);
|
||||
}
|
||||
next.add(userId);
|
||||
if (!agentPlayersMap.value[userId]) void loadExpansionData(userId);
|
||||
}
|
||||
expandedSet.value = next;
|
||||
}
|
||||
|
||||
async function loadExpansionData(agentUserId: string) {
|
||||
async function loadExpansionData(agentUserId: string, page = 1) {
|
||||
expandLoading.value[agentUserId] = true;
|
||||
try {
|
||||
const { data } = await api.get(`/agent/agents/${agentUserId}/players`);
|
||||
agentPlayersMap.value[agentUserId] = (data.data ?? []) as ScopedPlayerRow[];
|
||||
const { data } = await api.get(`/agent/agents/${agentUserId}/players`, {
|
||||
params: { page, pageSize: EXPAND_PLAYER_PAGE_SIZE },
|
||||
});
|
||||
const payload = data.data as {
|
||||
items: ScopedPlayerRow[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
agentPlayersMap.value[agentUserId] = payload.items ?? [];
|
||||
agentPlayersMeta.value[agentUserId] = {
|
||||
total: payload.total ?? 0,
|
||||
page: payload.page ?? page,
|
||||
pageSize: payload.pageSize ?? EXPAND_PLAYER_PAGE_SIZE,
|
||||
};
|
||||
} catch {
|
||||
agentPlayersMap.value[agentUserId] = [];
|
||||
agentPlayersMeta.value[agentUserId] = {
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: EXPAND_PLAYER_PAGE_SIZE,
|
||||
};
|
||||
} finally {
|
||||
expandLoading.value[agentUserId] = false;
|
||||
}
|
||||
@@ -474,6 +502,20 @@ function getPlayers(agentUserId: string) {
|
||||
return agentPlayersMap.value[agentUserId] ?? [];
|
||||
}
|
||||
|
||||
function getPlayersMeta(agentUserId: string) {
|
||||
return (
|
||||
agentPlayersMeta.value[agentUserId] ?? {
|
||||
total: getPlayers(agentUserId).length,
|
||||
page: 1,
|
||||
pageSize: EXPAND_PLAYER_PAGE_SIZE,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function onExpandPlayersPageChange(agentUserId: string, page: number) {
|
||||
void loadExpansionData(agentUserId, page);
|
||||
}
|
||||
|
||||
function refreshExpandedAgentPlayers() {
|
||||
for (const uid of expandedSet.value) {
|
||||
void loadExpansionData(uid);
|
||||
@@ -580,6 +622,60 @@ async function toggleFreeze(row: ScopedPlayerRow) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Delete ─── */
|
||||
async function deletePlayerRow(row: ScopedPlayerRow) {
|
||||
if (!canOperatePlayer(row)) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('msg.delete_player_body', { name: row.username }),
|
||||
t('msg.delete_player_title'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: t('common.delete'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
},
|
||||
);
|
||||
} catch { return; }
|
||||
const input = ref('');
|
||||
try {
|
||||
await ElMessageBox({
|
||||
title: t('msg.delete_player_confirm_title'),
|
||||
message: () =>
|
||||
h('div', {}, [
|
||||
h('p', { style: 'margin: 0 0 8px; font-size: 13px; color: var(--el-text-color-regular)' },
|
||||
t('msg.delete_player_confirm_hint', { name: row.username })),
|
||||
h('input', {
|
||||
value: input.value,
|
||||
placeholder: row.username,
|
||||
onInput: (e: Event) => { input.value = (e.target as HTMLInputElement).value; },
|
||||
style: 'width: 100%; padding: 6px 8px; border: 1px solid var(--el-border-color); border-radius: 4px; font-size: 13px; box-sizing: border-box',
|
||||
}),
|
||||
]),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: t('common.delete'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
beforeClose: (action: string, _instance: unknown, done: () => void) => {
|
||||
if (action === 'confirm') {
|
||||
if (input.value.trim() !== row.username) {
|
||||
ElMessage.warning(t('msg.delete_player_mismatch'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
done();
|
||||
},
|
||||
});
|
||||
} catch { return; }
|
||||
try {
|
||||
await api.delete(`/agent/players/${row.id}`);
|
||||
ElMessage.success(t('msg.delete_player_done'));
|
||||
loadAllPlayers();
|
||||
refreshExpandedAgentPlayers();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.delete_player_failed'));
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Transfer ─── */
|
||||
async function openTransfer(type: 'deposit' | 'withdraw', row: ScopedPlayerRow) {
|
||||
if (!canOperatePlayer(row)) return;
|
||||
@@ -938,6 +1034,7 @@ function statusTagType(s: string) {
|
||||
@deposit="openTransfer('deposit', row)"
|
||||
@withdraw="openTransfer('withdraw', row)"
|
||||
@freeze="toggleFreeze(row)"
|
||||
@delete="deletePlayerRow(row)"
|
||||
/>
|
||||
<el-button
|
||||
v-else-if="canViewPlayer(row)"
|
||||
@@ -1040,7 +1137,7 @@ function statusTagType(s: string) {
|
||||
<div v-else class="expand-panel-body">
|
||||
<p v-if="!isDirectChildAgent(row)" class="expand-readonly-hint">{{ expandReadonlyHint }}</p>
|
||||
<div class="expand-section-title">
|
||||
{{ directPlayersTabLabel(row.username, getPlayers(row.userId).length) }}
|
||||
{{ directPlayersTabLabel(row.username, getPlayersMeta(row.userId).total) }}
|
||||
</div>
|
||||
<el-table
|
||||
:data="getPlayers(row.userId)"
|
||||
@@ -1079,6 +1176,19 @@ function statusTagType(s: string) {
|
||||
<template #default="{ row: player }">{{ formatTime(player.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div
|
||||
v-if="getPlayersMeta(row.userId).total > getPlayersMeta(row.userId).pageSize"
|
||||
class="expand-pager"
|
||||
>
|
||||
<el-pagination
|
||||
small
|
||||
layout="total, prev, pager, next"
|
||||
:total="getPlayersMeta(row.userId).total"
|
||||
:current-page="getPlayersMeta(row.userId).page"
|
||||
:page-size="getPlayersMeta(row.userId).pageSize"
|
||||
@current-change="(p: number) => onExpandPlayersPageChange(row.userId, p)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1480,6 +1590,11 @@ function statusTagType(s: string) {
|
||||
.expand-section-title--spaced { margin-top: 14px; }
|
||||
.expand-readonly-hint { font-size: 12px; color: #999; margin: 0 0 8px; }
|
||||
.nested-table { margin-bottom: 4px; }
|
||||
.expand-pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* ─── Shared ─── */
|
||||
.field-hint { margin-top: 6px; font-size: 12px; color: #666; line-height: 1.4; }
|
||||
|
||||
Reference in New Issue
Block a user