diff --git a/src/api/admin/applications.ts b/src/api/admin/applications.ts index 2f5da78..ebca323 100644 --- a/src/api/admin/applications.ts +++ b/src/api/admin/applications.ts @@ -47,6 +47,48 @@ export async function getAdminApplication( return data as AdminApplicationDetail } +export async function impersonateAdminApplication( + competitionId: number, + applicationId: number, +): Promise<{ + token: string + competition_slug: string + expires_in: number + participant?: { id?: number; mobile?: string | null; name?: string | null } +}> { + try { + const { data } = await adminHttp.post( + `/competitions/${competitionId}/applications/${applicationId}/impersonate`, + ) + if (!data || typeof data !== 'object') throw new Error('代登响应无效') + const o = data as Record + if (typeof o.token !== 'string' || !o.token.trim()) throw new Error('代登 Token 无效') + if (typeof o.competition_slug !== 'string' || !o.competition_slug.trim()) { + throw new Error('赛事标识无效') + } + return { + token: o.token, + competition_slug: o.competition_slug, + expires_in: typeof o.expires_in === 'number' ? o.expires_in : 1800, + participant: + o.participant && typeof o.participant === 'object' + ? (o.participant as { id?: number; mobile?: string | null; name?: string | null }) + : undefined, + } + } catch (error) { + if (axios.isAxiosError(error)) { + const payload = error.response?.data + if (payload && typeof payload === 'object') { + const message = (payload as { message?: unknown }).message + if (typeof message === 'string' && message.trim()) { + throw new Error(message.trim()) + } + } + } + throw error instanceof Error ? error : new Error('进入选手端失败') + } +} + export async function downloadAdminApplicationFile( competitionId: number, applicationId: number, diff --git a/src/config/api.ts b/src/config/api.ts index 9442f57..169da48 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -13,6 +13,9 @@ export const PARTICIPANT_SMS_LOGIN_PATH = '/api/auth/sms/login' as const export const TOKEN_KEY = 'cxxfds_token' as const +/** 管理员代登选手端标记(sessionStorage) */ +export const ADMIN_IMPERSONATION_FLAG_KEY = 'cxxfds_admin_impersonation' as const + /** 选手端最近一次访问的赛事 slug(用于 / 与旧链接跳转) */ export const PARTICIPANT_COMPETITION_SLUG_KEY = 'cxxfds_participant_competition_slug' as const diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index cf420de..8d5928c 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -3,7 +3,7 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import 'bootstrap/dist/css/bootstrap.min.css' import '../styles/prototype-styles.css' -import { getApiBase, TOKEN_KEY } from '../config/api' +import { ADMIN_IMPERSONATION_FLAG_KEY, getApiBase, TOKEN_KEY } from '../config/api' import { brandingFormFromApi, emptyBrandingForm, @@ -101,8 +101,15 @@ function applyParticipantBodyTheme() { watch(brand, () => applyParticipantBodyTheme(), { deep: true, immediate: true }) const profileLabel = ref('') +const isAdminImpersonation = ref(sessionStorage.getItem(ADMIN_IMPERSONATION_FLAG_KEY) === '1') -const roleBadge = computed(() => profileLabel.value || '用户') +const roleBadge = computed(() => { + if (isAdminImpersonation.value) { + const label = profileLabel.value.trim() + return label ? `管理员代登 · ${label}` : '管理员代登中' + } + return profileLabel.value || '用户' +}) async function loadProfile() { const t = localStorage.getItem(TOKEN_KEY) @@ -123,6 +130,8 @@ async function loadProfile() { function logout() { localStorage.removeItem(TOKEN_KEY) localStorage.removeItem('cxxfds_user') + sessionStorage.removeItem(ADMIN_IMPERSONATION_FLAG_KEY) + isAdminImpersonation.value = false const s = participantSlug.value if (s) { void router.push({ name: 'participant-login', params: { slug: s } }) @@ -133,6 +142,7 @@ function logout() { onMounted(() => { document.body.classList.add('prototype-page', 'user-mobile-no-menu') + isAdminImpersonation.value = sessionStorage.getItem(ADMIN_IMPERSONATION_FLAG_KEY) === '1' loadProfile() }) @@ -147,6 +157,13 @@ onUnmounted(() => {