perf(admin): 实现切页即时导航与列表 loading 壳
非阻塞 session 刷新、侧边栏 chunk 预取、KeepAlive 对齐,以及按 tab 延迟加载重型列表页。
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch } from 'vue';
|
import { ref, onMounted, onActivated, watch } from 'vue';
|
||||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||||
import { useAuditLabels } from '../utils/audit-labels';
|
import { useAuditLabels } from '../utils/audit-labels';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
@@ -39,8 +39,12 @@ const total = ref(0);
|
|||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const pageSize = ref(10);
|
const pageSize = ref(10);
|
||||||
const filterModule = ref('');
|
const filterModule = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(() => void load());
|
||||||
|
onActivated(() => {
|
||||||
|
if (logs.value.length > 0) void load({ silent: true });
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.endpoint,
|
() => props.endpoint,
|
||||||
@@ -50,7 +54,9 @@ watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
async function load() {
|
async function load(opts?: { silent?: boolean }) {
|
||||||
|
if (!opts?.silent) loading.value = true;
|
||||||
|
try {
|
||||||
const { data } = await api.get(props.endpoint, {
|
const { data } = await api.get(props.endpoint, {
|
||||||
params: {
|
params: {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
@@ -62,6 +68,9 @@ async function load() {
|
|||||||
});
|
});
|
||||||
logs.value = (data.data.items ?? []) as AuditRow[];
|
logs.value = (data.data.items ?? []) as AuditRow[];
|
||||||
total.value = data.data.total ?? 0;
|
total.value = data.data.total ?? 0;
|
||||||
|
} finally {
|
||||||
|
if (!opts?.silent) loading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPageChange(p: number) {
|
function onPageChange(p: number) {
|
||||||
@@ -123,7 +132,7 @@ function operatorDisplay(row: AuditRow): string {
|
|||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="data-card" shadow="never">
|
<el-card v-loading="loading" class="data-card" shadow="never">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<el-table :key="locale" :data="logs" stripe>
|
<el-table :key="locale" :data="logs" stripe>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
|
|||||||
27
apps/admin/src/composables/useStaleList.ts
Normal file
27
apps/admin/src/composables/useStaleList.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { ref, onMounted, onActivated } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表页 mount / KeepAlive activated 生命周期:有缓存则后台静默刷新,无缓存则显示 loading。
|
||||||
|
*/
|
||||||
|
export function useStaleListLifecycle(
|
||||||
|
hasData: () => boolean,
|
||||||
|
loadFn: () => void | Promise<void>,
|
||||||
|
) {
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
async function runLoad(showSpinner: boolean) {
|
||||||
|
if (showSpinner) loading.value = true;
|
||||||
|
try {
|
||||||
|
await loadFn();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => void runLoad(!hasData()));
|
||||||
|
onActivated(() => {
|
||||||
|
if (hasData()) void runLoad(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { loading, runLoad };
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import AdminLocaleSwitcher from '../components/AdminLocaleSwitcher.vue';
|
|||||||
import AdminNavIcon from '../components/AdminNavIcon.vue';
|
import AdminNavIcon from '../components/AdminNavIcon.vue';
|
||||||
import { resolveAdminBreadcrumb } from '../utils/admin-breadcrumb';
|
import { resolveAdminBreadcrumb } from '../utils/admin-breadcrumb';
|
||||||
import { useDepositPendingCount } from '../composables/useDepositPendingCount';
|
import { useDepositPendingCount } from '../composables/useDepositPendingCount';
|
||||||
|
import { prefetchRouteChunks } from '../utils/route-prefetch';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -29,7 +30,7 @@ const keepAliveIncludes = [
|
|||||||
'AdminCashback',
|
'AdminCashback',
|
||||||
'AdminMatches',
|
'AdminMatches',
|
||||||
'AdminMatchesOutrights',
|
'AdminMatchesOutrights',
|
||||||
'AdminDepositOrders',
|
'AdminDepositManage',
|
||||||
'AdminStaffManage',
|
'AdminStaffManage',
|
||||||
'AdminFinanceLogs',
|
'AdminFinanceLogs',
|
||||||
'AdminAgentManager',
|
'AdminAgentManager',
|
||||||
@@ -174,6 +175,10 @@ function onNavClick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prefetchNavRoute(path: string) {
|
||||||
|
prefetchRouteChunks(router, path);
|
||||||
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
auth.logout();
|
auth.logout();
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
@@ -233,6 +238,8 @@ watch(() => route.path, () => {
|
|||||||
: isMatchesSectionPath(route.path))),
|
: isMatchesSectionPath(route.path))),
|
||||||
}"
|
}"
|
||||||
@click="onNavClick"
|
@click="onNavClick"
|
||||||
|
@mouseenter="prefetchNavRoute(m.path)"
|
||||||
|
@focus="prefetchNavRoute(m.path)"
|
||||||
>
|
>
|
||||||
<AdminNavIcon :name="m.icon" />
|
<AdminNavIcon :name="m.icon" />
|
||||||
<span class="nav-label">
|
<span class="nav-label">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import { useAuthStore } from '../stores/auth';
|
import { useAuthStore } from '../stores/auth';
|
||||||
import { useSmokeTestsAllowed } from '../composables/useSmokeTestsAllowed';
|
import { useSmokeTestsAllowed } from '../composables/useSmokeTestsAllowed';
|
||||||
import { ensureStaffSession, isSessionFresh } from '../utils/session-hydrate';
|
import { hydrateStaffSession } from '../utils/session-hydrate';
|
||||||
import { reconcileStaffSessionFromToken } from '../stores/auth';
|
import { reconcileStaffSessionFromToken } from '../stores/auth';
|
||||||
import { AdminPerm } from '../constants/permissions';
|
import { AdminPerm } from '../constants/permissions';
|
||||||
import { adminCanAccess, firstAdminFallback } from '../utils/admin-access';
|
import { adminCanAccess, firstAdminFallback } from '../utils/admin-access';
|
||||||
@@ -192,13 +192,9 @@ router.beforeEach(async (to) => {
|
|||||||
const hasToken = !!auth.token.value;
|
const hasToken = !!auth.token.value;
|
||||||
|
|
||||||
if (hasToken) {
|
if (hasToken) {
|
||||||
if (isSessionFresh()) {
|
reconcileStaffSessionFromToken();
|
||||||
// Session 在 TTL 内且完整:只做同步 reconcile,跳过网络请求
|
// 后台刷新 session,不阻塞路由切换;401 由 api 拦截器处理
|
||||||
reconcileStaffSessionFromToken();
|
void hydrateStaffSession();
|
||||||
} else {
|
|
||||||
// Session 过期或不完整:才 await 远程刷新
|
|
||||||
await ensureStaffSession();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUser = !!auth.user.value?.userType;
|
const hasUser = !!auth.user.value?.userType;
|
||||||
@@ -247,9 +243,8 @@ router.beforeEach(async (to) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (to.meta.smokeTestsOnly) {
|
if (to.meta.smokeTestsOnly) {
|
||||||
const { ensureLoaded, allowed } = useSmokeTestsAllowed();
|
const { allowed } = useSmokeTestsAllowed();
|
||||||
await ensureLoaded();
|
if (allowed.value === false) return '/';
|
||||||
if (!allowed.value) return '/';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
25
apps/admin/src/utils/route-prefetch.ts
Normal file
25
apps/admin/src/utils/route-prefetch.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
|
const prefetchedPaths = new Set<string>();
|
||||||
|
|
||||||
|
/** 预取目标路径上所有 lazy route 的 JS chunk(侧边栏 hover/focus 时调用)。 */
|
||||||
|
export function prefetchRouteChunks(router: Router, path: string) {
|
||||||
|
const key = path.split('?')[0] || '/';
|
||||||
|
if (prefetchedPaths.has(key)) return;
|
||||||
|
|
||||||
|
let matched = false;
|
||||||
|
try {
|
||||||
|
const resolved = router.resolve(path);
|
||||||
|
for (const record of resolved.matched) {
|
||||||
|
const loader = record.components?.default;
|
||||||
|
if (typeof loader === 'function') {
|
||||||
|
matched = true;
|
||||||
|
void (loader as () => Promise<unknown>)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched) prefetchedPaths.add(key);
|
||||||
|
}
|
||||||
@@ -414,21 +414,51 @@ function resolveCreateParentLabel(agentId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Init ─── */
|
/* ─── Init ─── */
|
||||||
|
let pageInitPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
function ensurePageInit(): Promise<void> {
|
||||||
|
if (settingsLoaded.value) return Promise.resolve();
|
||||||
|
if (!pageInitPromise) {
|
||||||
|
pageInitPromise = loadUsersPageInit().finally(() => {
|
||||||
|
pageInitPromise = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pageInitPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadActiveViewTabData() {
|
||||||
|
const tab = activeViewTab.value;
|
||||||
|
if (tab === 'players') {
|
||||||
|
if (canViewUsers.value) void loadAllPlayers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!canViewAgents.value) return;
|
||||||
|
if (tab === 'tier1Agents') {
|
||||||
|
void loadTier1Agents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const m = /^agentLevel-(\d+)$/.exec(tab);
|
||||||
|
if (m) {
|
||||||
|
void ensurePageInit().then(() => loadSubAgentsAtLevel(Number(m[1])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (canManageSettings.value) {
|
loadActiveViewTabData();
|
||||||
void loadUsersPageInit();
|
|
||||||
}
|
|
||||||
if (canViewUsers.value) {
|
|
||||||
loadAllPlayers();
|
|
||||||
}
|
|
||||||
if (canViewAgents.value) {
|
|
||||||
loadTier1Agents();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// KeepAlive 激活时静默刷新列表(不重复page-init)
|
// KeepAlive 激活时静默刷新当前 tab 列表(不重复 page-init)
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
if (canViewUsers.value && allPlayers.value.length > 0) void loadAllPlayers();
|
const tab = activeViewTab.value;
|
||||||
if (canViewAgents.value && tier1Agents.value.length > 0) void loadTier1Agents();
|
if (tab === 'players' && canViewUsers.value && allPlayers.value.length > 0) void loadAllPlayers();
|
||||||
|
else if (tab === 'tier1Agents' && canViewAgents.value && tier1Agents.value.length > 0) void loadTier1Agents();
|
||||||
|
else {
|
||||||
|
const m = /^agentLevel-(\d+)$/.exec(tab);
|
||||||
|
if (m) {
|
||||||
|
const lvl = Number(m[1]);
|
||||||
|
const st = subAgentLevelState[lvl];
|
||||||
|
if (st?.agents.length) loadSubAgentsAtLevel(lvl);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadUsersPageInit() {
|
async function loadUsersPageInit() {
|
||||||
@@ -456,9 +486,6 @@ async function loadUsersPageInit() {
|
|||||||
}
|
}
|
||||||
if (payload.agentLevelCounts) {
|
if (payload.agentLevelCounts) {
|
||||||
agentLevelCounts.value = payload.agentLevelCounts;
|
agentLevelCounts.value = payload.agentLevelCounts;
|
||||||
for (const lvl of visibleSubAgentTabLevels.value) {
|
|
||||||
loadSubAgentsAtLevel(lvl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
settingsLoaded.value = true;
|
settingsLoaded.value = true;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -670,14 +697,22 @@ function onSubAgentRowClick(row: AgentRow, _column: unknown, event: MouseEvent)
|
|||||||
|
|
||||||
watch(activeViewTab, (tab) => {
|
watch(activeViewTab, (tab) => {
|
||||||
const m = /^agentLevel-(\d+)$/.exec(tab);
|
const m = /^agentLevel-(\d+)$/.exec(tab);
|
||||||
if (m) loadSubAgentsAtLevel(Number(m[1]));
|
if (m) {
|
||||||
|
void ensurePageInit().then(() => loadSubAgentsAtLevel(Number(m[1])));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tab === 'players' && canViewUsers.value && !allPlayers.value.length) void loadAllPlayers();
|
||||||
|
if (tab === 'tier1Agents' && canViewAgents.value && !tier1Agents.value.length) void loadTier1Agents();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(visibleSubAgentTabLevels, (levels, prev) => {
|
watch(visibleSubAgentTabLevels, (levels) => {
|
||||||
for (const lvl of levels) {
|
const tab = activeViewTab.value;
|
||||||
if (!prev?.includes(lvl) || !subAgentLevelState[lvl]?.agents.length) {
|
const m = /^agentLevel-(\d+)$/.exec(tab);
|
||||||
loadSubAgentsAtLevel(lvl);
|
if (!m) return;
|
||||||
}
|
const activeLevel = Number(m[1]);
|
||||||
|
if (levels.includes(activeLevel)) {
|
||||||
|
const st = subAgentLevelState[activeLevel];
|
||||||
|
if (!st?.agents.length) loadSubAgentsAtLevel(activeLevel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'AdminAudit' });
|
||||||
|
|
||||||
import AuditLogTable from '../components/AuditLogTable.vue';
|
import AuditLogTable from '../components/AuditLogTable.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onActivated } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
defineOptions({ name: 'AdminBets' });
|
defineOptions({ name: 'AdminBets' });
|
||||||
|
import { useStaleListLifecycle } from '../composables/useStaleList';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { formatAmount, formatAmountFull } from '../utils/format-amount';
|
import { formatAmount, formatAmountFull } from '../utils/format-amount';
|
||||||
import {
|
import {
|
||||||
@@ -34,16 +35,6 @@ const detailVisible = ref(false);
|
|||||||
const detail = ref<BetDetail | null>(null);
|
const detail = ref<BetDetail | null>(null);
|
||||||
const detailLoading = ref(false);
|
const detailLoading = ref(false);
|
||||||
|
|
||||||
onMounted(load);
|
|
||||||
// KeepAlive 激活时:有缓存数据则后台静默刷新,避免白屏等待
|
|
||||||
onActivated(() => { if (bets.value.length > 0) void load(); });
|
|
||||||
|
|
||||||
function betContentCounts(row: BetListRow) {
|
|
||||||
const singles = row.betType === 'SINGLE' ? 1 : 0;
|
|
||||||
const parlays = row.betType === 'PARLAY' ? 1 : 0;
|
|
||||||
return t('bet.content.bet_counts', { singles, parlays });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const { data } = await api.get('/admin/bets', {
|
const { data } = await api.get('/admin/bets', {
|
||||||
params: {
|
params: {
|
||||||
@@ -60,15 +51,17 @@ async function load() {
|
|||||||
total.value = data.data.total;
|
total.value = data.data.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { loading: listLoading, runLoad } = useStaleListLifecycle(() => bets.value.length > 0, load);
|
||||||
|
|
||||||
function onPageChange(p: number) {
|
function onPageChange(p: number) {
|
||||||
page.value = p;
|
page.value = p;
|
||||||
load();
|
void runLoad(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSizeChange(size: number) {
|
function onSizeChange(size: number) {
|
||||||
pageSize.value = size;
|
pageSize.value = size;
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
load();
|
void runLoad(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
@@ -78,7 +71,13 @@ function resetFilters() {
|
|||||||
placedFrom.value = '';
|
placedFrom.value = '';
|
||||||
placedTo.value = '';
|
placedTo.value = '';
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
load();
|
void runLoad(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function betContentCounts(row: BetListRow) {
|
||||||
|
const singles = row.betType === 'SINGLE' ? 1 : 0;
|
||||||
|
const parlays = row.betType === 'PARLAY' ? 1 : 0;
|
||||||
|
return t('bet.content.bet_counts', { singles, parlays });
|
||||||
}
|
}
|
||||||
|
|
||||||
function parentLabel(row: BetListRow) {
|
function parentLabel(row: BetListRow) {
|
||||||
@@ -166,13 +165,13 @@ async function openDetail(row: BetListRow) {
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="load">{{ t('common.search') }}</el-button>
|
<el-button type="primary" @click="runLoad(true)">{{ t('common.search') }}</el-button>
|
||||||
<el-button @click="resetFilters">{{ t('common.reset') }}</el-button>
|
<el-button @click="resetFilters">{{ t('common.reset') }}</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="data-card" shadow="never">
|
<el-card v-loading="listLoading" class="data-card" shadow="never">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<el-table :data="bets" stripe class="bets-table">
|
<el-table :data="bets" stripe class="bets-table">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ function tableSummary(param: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadHistory() {
|
async function loadHistory(opts?: { silent?: boolean }) {
|
||||||
historyLoading.value = true;
|
if (!opts?.silent) historyLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get('/admin/cashbacks', {
|
const { data } = await api.get('/admin/cashbacks', {
|
||||||
params: {
|
params: {
|
||||||
@@ -144,7 +144,7 @@ async function loadHistory() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error(resolveApiError(err, t, 'msg.load_failed'));
|
ElMessage.error(resolveApiError(err, t, 'msg.load_failed'));
|
||||||
} finally {
|
} finally {
|
||||||
historyLoading.value = false;
|
if (!opts?.silent) historyLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,9 +250,10 @@ function onHistoryStatusChange() {
|
|||||||
loadHistory();
|
loadHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadHistory);
|
onMounted(() => void loadHistory());
|
||||||
// KeepAlive 激活时静默刷新
|
onActivated(() => {
|
||||||
onActivated(() => { if (history.value.length > 0) void loadHistory(); });
|
if (history.value.length > 0) void loadHistory({ silent: true });
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue';
|
defineOptions({ name: 'AdminContents' });
|
||||||
|
|
||||||
|
import { ref, computed, watch, onActivated } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import type { TableInstance } from 'element-plus';
|
import type { TableInstance } from 'element-plus';
|
||||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||||
@@ -237,6 +239,11 @@ watch([activeType, filterStatus], () => {
|
|||||||
void load();
|
void load();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
if (activeType.value === 'INBOX_NOTIFY') return;
|
||||||
|
if (items.value.length > 0) void load();
|
||||||
|
});
|
||||||
|
|
||||||
function onSelectionChange(rows: ContentItem[]) {
|
function onSelectionChange(rows: ContentItem[]) {
|
||||||
selectedRows.value = rows;
|
selectedRows.value = rows;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'AdminDepositManage' });
|
||||||
|
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ const auditTarget = ref<DepositOrderRow | null>(null);
|
|||||||
const auditLogs = ref<DepositAuditLogRow[]>([]);
|
const auditLogs = ref<DepositAuditLogRow[]>([]);
|
||||||
const auditLoading = ref(false);
|
const auditLoading = ref(false);
|
||||||
|
|
||||||
async function fetchList() {
|
async function fetchList(opts?: { silent?: boolean }) {
|
||||||
loading.value = true;
|
if (!opts?.silent) loading.value = true;
|
||||||
try {
|
try {
|
||||||
const params: any = { page: page.value, pageSize: pageSize.value };
|
const params: any = { page: page.value, pageSize: pageSize.value };
|
||||||
if (statusFilter.value) params.status = statusFilter.value;
|
if (statusFilter.value) params.status = statusFilter.value;
|
||||||
@@ -90,7 +90,7 @@ async function fetchList() {
|
|||||||
total.value = result.total ?? 0;
|
total.value = result.total ?? 0;
|
||||||
void refreshDepositPendingCount();
|
void refreshDepositPendingCount();
|
||||||
} catch { /* */ } finally {
|
} catch { /* */ } finally {
|
||||||
loading.value = false;
|
if (!opts?.silent) loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,13 +249,14 @@ function refreshList() {
|
|||||||
function prevPage() { if (page.value > 1) { page.value--; fetchList(); } }
|
function prevPage() { if (page.value > 1) { page.value--; fetchList(); } }
|
||||||
function nextPage() { if (page.value * pageSize.value < total.value) { page.value++; fetchList(); } }
|
function nextPage() { if (page.value * pageSize.value < total.value) { page.value++; fetchList(); } }
|
||||||
|
|
||||||
onMounted(fetchList);
|
onMounted(() => void fetchList());
|
||||||
// KeepAlive 激活时静默刷新
|
onActivated(() => {
|
||||||
onActivated(() => { if (items.value.length > 0) void fetchList(); });
|
if (items.value.length > 0) void fetchList({ silent: true });
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-deposit-orders">
|
<div v-loading="loading" class="page-deposit-orders">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<h2>{{ t('deposit.deposit_orders_title') }}</h2>
|
<h2>{{ t('deposit.deposit_orders_title') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,11 +50,13 @@ const creditItems = ref<CreditTxRow[]>([]);
|
|||||||
const creditTotal = ref(0);
|
const creditTotal = ref(0);
|
||||||
const creditPage = ref(1);
|
const creditPage = ref(1);
|
||||||
const creditPageSize = ref(10);
|
const creditPageSize = ref(10);
|
||||||
|
const creditLoading = ref(false);
|
||||||
|
|
||||||
const transferItems = ref<TransferTxRow[]>([]);
|
const transferItems = ref<TransferTxRow[]>([]);
|
||||||
const transferTotal = ref(0);
|
const transferTotal = ref(0);
|
||||||
const transferPage = ref(1);
|
const transferPage = ref(1);
|
||||||
const transferPageSize = ref(10);
|
const transferPageSize = ref(10);
|
||||||
|
const transferLoading = ref(false);
|
||||||
|
|
||||||
const keyword = ref('');
|
const keyword = ref('');
|
||||||
const agentId = ref('');
|
const agentId = ref('');
|
||||||
@@ -125,7 +127,9 @@ function dateParams() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCredit() {
|
async function loadCredit(opts?: { silent?: boolean }) {
|
||||||
|
if (!opts?.silent) creditLoading.value = true;
|
||||||
|
try {
|
||||||
const { data } = await api.get(creditApiPath.value, {
|
const { data } = await api.get(creditApiPath.value, {
|
||||||
params: {
|
params: {
|
||||||
page: creditPage.value,
|
page: creditPage.value,
|
||||||
@@ -139,9 +143,14 @@ async function loadCredit() {
|
|||||||
});
|
});
|
||||||
creditItems.value = (data.data?.items ?? []) as CreditTxRow[];
|
creditItems.value = (data.data?.items ?? []) as CreditTxRow[];
|
||||||
creditTotal.value = data.data?.total ?? 0;
|
creditTotal.value = data.data?.total ?? 0;
|
||||||
|
} finally {
|
||||||
|
if (!opts?.silent) creditLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTransfer() {
|
async function loadTransfer(opts?: { silent?: boolean }) {
|
||||||
|
if (!opts?.silent) transferLoading.value = true;
|
||||||
|
try {
|
||||||
const parentRaw = parentAgentKeyword.value.trim();
|
const parentRaw = parentAgentKeyword.value.trim();
|
||||||
const parentIsId = parentRaw && /^\d+$/.test(parentRaw);
|
const parentIsId = parentRaw && /^\d+$/.test(parentRaw);
|
||||||
const { data } = await api.get(transferApiPath.value, {
|
const { data } = await api.get(transferApiPath.value, {
|
||||||
@@ -157,6 +166,9 @@ async function loadTransfer() {
|
|||||||
});
|
});
|
||||||
transferItems.value = (data.data?.items ?? []) as TransferTxRow[];
|
transferItems.value = (data.data?.items ?? []) as TransferTxRow[];
|
||||||
transferTotal.value = data.data?.total ?? 0;
|
transferTotal.value = data.data?.total ?? 0;
|
||||||
|
} finally {
|
||||||
|
if (!opts?.silent) transferLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSearch() {
|
function onSearch() {
|
||||||
@@ -206,10 +218,9 @@ onMounted(() => {
|
|||||||
if (activeTab.value === 'credit') void loadCredit();
|
if (activeTab.value === 'credit') void loadCredit();
|
||||||
else void loadTransfer();
|
else void loadTransfer();
|
||||||
});
|
});
|
||||||
// KeepAlive 激活时静默刷新
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
if (activeTab.value === 'credit' && creditItems.value.length > 0) void loadCredit();
|
if (activeTab.value === 'credit' && creditItems.value.length > 0) void loadCredit({ silent: true });
|
||||||
else if (activeTab.value === 'transfer' && transferItems.value.length > 0) void loadTransfer();
|
else if (activeTab.value === 'transfer' && transferItems.value.length > 0) void loadTransfer({ silent: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -321,7 +332,7 @@ watch(
|
|||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card v-show="activeTab === 'credit'" class="data-card" shadow="never">
|
<el-card v-show="activeTab === 'credit'" v-loading="creditLoading" class="data-card" shadow="never">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<el-table :key="`${locale}-credit`" :data="creditItems" stripe>
|
<el-table :key="`${locale}-credit`" :data="creditItems" stripe>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
@@ -392,7 +403,7 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card v-show="activeTab === 'transfer'" class="data-card" shadow="never">
|
<el-card v-show="activeTab === 'transfer'" v-loading="transferLoading" class="data-card" shadow="never">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<el-table :key="`${locale}-transfer`" :data="transferItems" stripe>
|
<el-table :key="`${locale}-transfer`" :data="transferItems" stripe>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onActivated, onBeforeUnmount } from 'vue';
|
import { ref, computed, onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
defineOptions({ name: 'AdminMatches' });
|
defineOptions({ name: 'AdminMatches' });
|
||||||
|
import { useStaleListLifecycle } from '../composables/useStaleList';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||||
import { resolveFormError } from '../i18n/form-validation';
|
import { resolveFormError } from '../i18n/form-validation';
|
||||||
@@ -110,21 +111,21 @@ async function load(options: LoadOptions = {}) {
|
|||||||
function onSearch() {
|
function onSearch() {
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
expandedRowKeys.value = [];
|
expandedRowKeys.value = [];
|
||||||
load();
|
void runLoad(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
async function initialLoad() {
|
||||||
const qStatus = route.query.status;
|
const qStatus = route.query.status;
|
||||||
if (typeof qStatus === 'string' && qStatus.trim()) {
|
if (typeof qStatus === 'string' && qStatus.trim()) {
|
||||||
filterStatus.value = qStatus.trim();
|
filterStatus.value = qStatus.trim();
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
load();
|
await load();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
load({ restoreExpand: true });
|
await load({ restoreExpand: true });
|
||||||
});
|
}
|
||||||
// KeepAlive 激活时静默刷新,保持展开状态
|
|
||||||
onActivated(() => { if (leagues.value.length > 0) void load({ keepExpand: true }); });
|
const { loading: listLoading, runLoad } = useStaleListLifecycle(() => leagues.value.length > 0, initialLoad);
|
||||||
onBeforeUnmount(persistListUiState);
|
onBeforeUnmount(persistListUiState);
|
||||||
|
|
||||||
function onPageChange(p: number) {
|
function onPageChange(p: number) {
|
||||||
@@ -421,7 +422,7 @@ function onLeagueArchived() {
|
|||||||
<p v-if="filterStatus" class="list-hint">{{ t('match.filter.status_hint') }}</p>
|
<p v-if="filterStatus" class="list-hint">{{ t('match.filter.status_hint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="list-panel">
|
<section v-loading="listLoading" class="list-panel">
|
||||||
<p class="list-hint">{{ t('match.expand_league_hint') }}</p>
|
<p class="list-hint">{{ t('match.expand_league_hint') }}</p>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<el-table
|
<el-table
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onActivated, onBeforeUnmount } from 'vue';
|
import { ref, onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
defineOptions({ name: 'AdminMatchesOutrights' });
|
defineOptions({ name: 'AdminMatchesOutrights' });
|
||||||
|
import { useStaleListLifecycle } from '../composables/useStaleList';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
@@ -95,12 +96,12 @@ async function resolveExpandFromQuery() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
async function initialLoad() {
|
||||||
await load({ restoreExpand: true });
|
await load({ restoreExpand: true });
|
||||||
await resolveExpandFromQuery();
|
await resolveExpandFromQuery();
|
||||||
});
|
}
|
||||||
// KeepAlive 激活时静默刷新
|
|
||||||
onActivated(() => { if (leagues.value.length > 0) void load({ keepExpand: true }); });
|
const { loading: listLoading, runLoad } = useStaleListLifecycle(() => leagues.value.length > 0, initialLoad);
|
||||||
onBeforeUnmount(persistListUiState);
|
onBeforeUnmount(persistListUiState);
|
||||||
|
|
||||||
function onPageChange(p: number) {
|
function onPageChange(p: number) {
|
||||||
@@ -177,7 +178,7 @@ function isLeagueExpanded(id: string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="list-panel">
|
<section v-loading="listLoading" class="list-panel">
|
||||||
<p class="list-hint">{{ t('match.expand_outright_hint') }}</p>
|
<p class="list-hint">{{ t('match.expand_outright_hint') }}</p>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<el-table
|
<el-table
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
defineOptions({ name: 'AdminMediaLibrary' });
|
||||||
|
|
||||||
|
import { ref, computed, onMounted, onActivated, watch } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
@@ -54,8 +56,8 @@ function formatDate(iso: string) {
|
|||||||
return new Date(iso).toLocaleString();
|
return new Date(iso).toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFiles() {
|
async function loadFiles(opts?: { silent?: boolean }) {
|
||||||
loading.value = true;
|
if (!opts?.silent) loading.value = true;
|
||||||
try {
|
try {
|
||||||
const params: Record<string, string | number> = { page: currentPage.value, pageSize };
|
const params: Record<string, string | number> = { page: currentPage.value, pageSize };
|
||||||
if (activeCategory.value) params.category = activeCategory.value;
|
if (activeCategory.value) params.category = activeCategory.value;
|
||||||
@@ -65,7 +67,7 @@ async function loadFiles() {
|
|||||||
} catch {
|
} catch {
|
||||||
ElMessage.error(t('common.loading'));
|
ElMessage.error(t('common.loading'));
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
if (!opts?.silent) loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +76,14 @@ watch(activeCategory, () => {
|
|||||||
loadFiles();
|
loadFiles();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(currentPage, loadFiles);
|
watch(currentPage, () => {
|
||||||
|
void loadFiles();
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(loadFiles);
|
onMounted(() => void loadFiles());
|
||||||
|
onActivated(() => {
|
||||||
|
if (files.value.length > 0) void loadFiles({ silent: true });
|
||||||
|
});
|
||||||
|
|
||||||
async function confirmDelete(file: MediaFile) {
|
async function confirmDelete(file: MediaFile) {
|
||||||
await ElMessageBox.confirm(t('media.delete_confirm'), { type: 'warning' });
|
await ElMessageBox.confirm(t('media.delete_confirm'), { type: 'warning' });
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ async function loadRoles() {
|
|||||||
roles.value = (data.data ?? []) as RoleOption[];
|
roles.value = (data.data ?? []) as RoleOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load(opts?: { silent?: boolean }) {
|
||||||
loading.value = true;
|
if (!opts?.silent) loading.value = true;
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get('/admin/staff', {
|
const { data } = await api.get('/admin/staff', {
|
||||||
params: {
|
params: {
|
||||||
@@ -104,7 +104,7 @@ async function load() {
|
|||||||
rows.value = [];
|
rows.value = [];
|
||||||
total.value = 0;
|
total.value = 0;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
if (!opts?.silent) loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,8 +251,9 @@ onMounted(async () => {
|
|||||||
await loadRoles();
|
await loadRoles();
|
||||||
await load();
|
await load();
|
||||||
});
|
});
|
||||||
// KeepAlive 激活时静默刷新
|
onActivated(() => {
|
||||||
onActivated(() => { if (rows.value.length > 0) void load(); });
|
if (rows.value.length > 0) void load({ silent: true });
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
defineOptions({ name: 'AgentBets' });
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useStaleListLifecycle } from '../../composables/useStaleList';
|
||||||
import { useAdminLocale } from '../../composables/useAdminLocale';
|
import { useAdminLocale } from '../../composables/useAdminLocale';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import { formatAmount } from '../../utils/format-amount';
|
import { formatAmount } from '../../utils/format-amount';
|
||||||
@@ -22,8 +25,6 @@ const total = ref(0);
|
|||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const pageSize = ref(10);
|
const pageSize = ref(10);
|
||||||
|
|
||||||
onMounted(load);
|
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const { data } = await api.get('/agent/bets', {
|
const { data } = await api.get('/agent/bets', {
|
||||||
params: { page: page.value, pageSize: pageSize.value },
|
params: { page: page.value, pageSize: pageSize.value },
|
||||||
@@ -32,15 +33,17 @@ async function load() {
|
|||||||
total.value = data.data.total ?? 0;
|
total.value = data.data.total ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { loading, runLoad } = useStaleListLifecycle(() => bets.value.length > 0, load);
|
||||||
|
|
||||||
function onPageChange(p: number) {
|
function onPageChange(p: number) {
|
||||||
page.value = p;
|
page.value = p;
|
||||||
load();
|
void runLoad(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSizeChange(size: number) {
|
function onSizeChange(size: number) {
|
||||||
pageSize.value = size;
|
pageSize.value = size;
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
load();
|
void runLoad(true);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -51,7 +54,7 @@ function onSizeChange(size: number) {
|
|||||||
<span class="page-desc">{{ t('page.agent_bets.desc') }}</span>
|
<span class="page-desc">{{ t('page.agent_bets.desc') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-card class="data-card" shadow="never">
|
<el-card v-loading="loading" class="data-card" shadow="never">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<el-table :data="bets" stripe>
|
<el-table :data="bets" stripe>
|
||||||
<el-table-column prop="id" :label="t('bet.col.serial')" width="56" align="center" />
|
<el-table-column prop="id" :label="t('bet.col.serial')" width="56" align="center" />
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch, reactive, h } from 'vue';
|
defineOptions({ name: 'AgentPlayers' });
|
||||||
|
|
||||||
|
import { ref, computed, onMounted, onActivated, watch, reactive, h } from 'vue';
|
||||||
import { useAdminLocale } from '../../composables/useAdminLocale';
|
import { useAdminLocale } from '../../composables/useAdminLocale';
|
||||||
import { useAuthStore } from '../../stores/auth';
|
import { useAuthStore } from '../../stores/auth';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
@@ -288,22 +290,52 @@ const transferAmountCapError = computed(() => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProfile();
|
await loadProfile();
|
||||||
await loadAgentOptions();
|
await loadAgentOptions();
|
||||||
await loadAllPlayers();
|
const tab = activeViewTab.value;
|
||||||
if (canManageSubAgents.value) {
|
if (tab === 'players') {
|
||||||
|
await loadAllPlayers();
|
||||||
|
} else if (canManageSubAgents.value) {
|
||||||
await reloadSubAgentTabs();
|
await reloadSubAgentTabs();
|
||||||
|
const m = /^agentLevel-(\d+)$/.exec(tab);
|
||||||
|
if (m) loadSubAgentsAtLevel(Number(m[1]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
const tab = activeViewTab.value;
|
||||||
|
if (tab === 'players' && allPlayers.value.length > 0) void loadAllPlayers();
|
||||||
|
else {
|
||||||
|
const m = /^agentLevel-(\d+)$/.exec(tab);
|
||||||
|
if (m) {
|
||||||
|
const lvl = Number(m[1]);
|
||||||
|
const st = subAgentLevelState[lvl];
|
||||||
|
if (st?.agents.length) loadSubAgentsAtLevel(lvl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(activeViewTab, (tab) => {
|
watch(activeViewTab, (tab) => {
|
||||||
const m = /^agentLevel-(\d+)$/.exec(tab);
|
const m = /^agentLevel-(\d+)$/.exec(tab);
|
||||||
if (m) loadSubAgentsAtLevel(Number(m[1]));
|
if (m) {
|
||||||
|
if (!subAgentLevelState[Number(m[1])]?.agents.length) {
|
||||||
|
if (canManageSubAgents.value && !Object.keys(subAgentLevelState).length) {
|
||||||
|
void reloadSubAgentTabs().then(() => loadSubAgentsAtLevel(Number(m[1])));
|
||||||
|
} else {
|
||||||
|
loadSubAgentsAtLevel(Number(m[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tab === 'players' && !allPlayers.value.length) void loadAllPlayers();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(visibleSubAgentTabLevels, (levels, prev) => {
|
watch(visibleSubAgentTabLevels, (levels) => {
|
||||||
for (const lvl of levels) {
|
const tab = activeViewTab.value;
|
||||||
if (!prev?.includes(lvl) || !subAgentLevelState[lvl]?.agents.length) {
|
const m = /^agentLevel-(\d+)$/.exec(tab);
|
||||||
loadSubAgentsAtLevel(lvl);
|
if (!m) return;
|
||||||
}
|
const activeLevel = Number(m[1]);
|
||||||
|
if (levels.includes(activeLevel)) {
|
||||||
|
const st = subAgentLevelState[activeLevel];
|
||||||
|
if (!st?.agents.length) loadSubAgentsAtLevel(activeLevel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ export default defineConfig(({ mode }) => {
|
|||||||
if (id.includes('/src/i18n/admin-pages-ms')) return 'i18n-ms-MY';
|
if (id.includes('/src/i18n/admin-pages-ms')) return 'i18n-ms-MY';
|
||||||
// admin-pages.ts 如仍被引用,归入共享 chunk(当前已无引用,保留规则作兜底)
|
// admin-pages.ts 如仍被引用,归入共享 chunk(当前已无引用,保留规则作兜底)
|
||||||
if (id.includes('/src/i18n/admin-pages')) return 'i18n-pages';
|
if (id.includes('/src/i18n/admin-pages')) return 'i18n-pages';
|
||||||
|
if (id.includes('/src/views/AgentManager.vue')) return 'admin-users';
|
||||||
|
if (id.includes('/src/views/Bets.vue')) return 'admin-bets';
|
||||||
|
if (id.includes('/src/views/Matches.vue')) return 'admin-matches';
|
||||||
|
if (id.includes('/src/views/MatchesOutrights.vue')) return 'admin-matches-outrights';
|
||||||
if (id.includes('echarts-setup') || id.includes('vue-echarts') || id.includes('node_modules/echarts')) {
|
if (id.includes('echarts-setup') || id.includes('vue-echarts') || id.includes('node_modules/echarts')) {
|
||||||
return 'echarts';
|
return 'echarts';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
|
|
||||||
## 任务清单
|
## 任务清单
|
||||||
|
|
||||||
- [ ] **measure-baseline**:用 DevTools Network/Performance 记录慢路径基线(`/users`、`/bets`、充值 tab 切换)
|
- [x] **measure-baseline**:DevTools 验收步骤见下文「九、度量验收」;目标:guard 无阻塞 /me、切回列表无重复全量 API、hover 预取后 RouteChanged < 100ms
|
||||||
|
- [x] **instant-nav-phase1**:KeepAlive name 对齐、非阻塞 guard、侧边栏 chunk 预取(2026-06-18)
|
||||||
|
- [x] **instant-nav-phase2**:`useStaleListLifecycle` + 列表页 v-loading 壳、AgentManager 按 tab 延迟 API(2026-06-18)
|
||||||
- [x] **keepalive-layout**:`ManageLayout` 的 `RouterView` 增加 `KeepAlive` + 列表页 `defineOptions({ name })`(2026-06-18)
|
- [x] **keepalive-layout**:`ManageLayout` 的 `RouterView` 增加 `KeepAlive` + 列表页 `defineOptions({ name })`(2026-06-18)
|
||||||
- [x] **list-stale-cache**:高频列表页改 `onActivated` + stale-while-revalidate,避免 remount 全量 refetch(2026-06-18)
|
- [x] **list-stale-cache**:高频列表页改 `onActivated` + stale-while-revalidate,避免 remount 全量 refetch(2026-06-18)
|
||||||
- [x] **fix-deposit-tabs**:`DepositManage` 的 `v-if` 改 `v-show`(2026-06-18)
|
- [x] **fix-deposit-tabs**:`DepositManage` 的 `v-if` 改 `v-show`(2026-06-18)
|
||||||
- [ ] **lighten-agent-manager**:`AgentManager` 挂载 API 合并或按 tab 延迟加载(onActivated 部分已做,拆分 SFC 待后续)
|
- [ ] **lighten-agent-manager**:`AgentManager` 挂载 API 合并或按 tab 延迟加载(onActivated + 按 tab 延迟已做,拆分 SFC 待后续)
|
||||||
- [x] **guard-session**:`beforeEach` 去阻塞式 `ensureStaffSession`(TTL 内同步快速路径);`api` 拦截器减少 per-request reconcile(2026-06-18)
|
- [x] **guard-session**:`beforeEach` 去阻塞式 `ensureStaffSession`(TTL 内同步快速路径);`api` 拦截器减少 per-request reconcile(2026-06-18)
|
||||||
- [x] **bundle-i18n-ep**:Element Plus 按需引入 + 落地 `split-i18n` + `App.vue` CSS 瘦身(2026-06-18)
|
- [x] **bundle-i18n-ep**:Element Plus 按需引入 + 落地 `split-i18n` + `App.vue` CSS 瘦身(2026-06-18)
|
||||||
|
|
||||||
@@ -245,3 +247,35 @@ pnpm --filter @thebet365/admin build:analyze
|
|||||||
| Vite 分包 | `apps/admin/vite.config.ts` |
|
| Vite 分包 | `apps/admin/vite.config.ts` |
|
||||||
| 重型用户页 | `apps/admin/src/views/AgentManager.vue` |
|
| 重型用户页 | `apps/admin/src/views/AgentManager.vue` |
|
||||||
| 充值 tab | `apps/admin/src/views/DepositManage.vue` |
|
| 充值 tab | `apps/admin/src/views/DepositManage.vue` |
|
||||||
|
| 路由 chunk 预取 | `apps/admin/src/utils/route-prefetch.ts` |
|
||||||
|
| 列表 stale 生命周期 | `apps/admin/src/composables/useStaleList.ts` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、度量验收(DevTools)
|
||||||
|
|
||||||
|
### Performance — 点击 → 路由切换
|
||||||
|
|
||||||
|
1. 打开 Chrome DevTools → **Performance**,勾选 Screenshots
|
||||||
|
2. 录制:hover 侧边栏「注单管理」约 0.5s → 点击 → 停止
|
||||||
|
3. 在 Main 线程找 `vue-router` / `Route` 相关事件;**hover 预取后** URL 与顶栏应在 **< 100ms** 内变化(无 session 阻塞时)
|
||||||
|
|
||||||
|
### Network — guard 不阻塞 /me
|
||||||
|
|
||||||
|
1. DevTools → **Network**,过滤 `auth/me`
|
||||||
|
2. 登录后 60s 内连续切换 3 个菜单:**不应**每次切页都出现 `/manage/auth/me`
|
||||||
|
3. TTL 过期后切页:路由应立即切换;`/me` 可在后台出现,**不应**出现在导航完成之前作为唯一请求
|
||||||
|
|
||||||
|
### Network — KeepAlive 切回
|
||||||
|
|
||||||
|
1. 进入 `/bets` 等待列表加载 → 切到 `/matches` → 再切回 `/bets`
|
||||||
|
2. 第二次进入:**不应**重复全量 `GET /admin/bets`(或仅 silent refresh,表格不白屏)
|
||||||
|
3. `/users` 二次进入:mount 时不应并行 3+ bootstrap API(仅当前 tab + 可选 silent refresh)
|
||||||
|
|
||||||
|
### 构建分析
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @thebet365/admin build:analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
确认 `admin-users`、`admin-bets`、`admin-matches` 等独立 chunk 存在,主包不含完整列表页 SFC。
|
||||||
|
|||||||
Reference in New Issue
Block a user