257 lines
7.2 KiB
Vue
257 lines
7.2 KiB
Vue
<template>
|
|
<el-config-provider>
|
|
<AdminLoginPanel
|
|
v-if="!adminSessionState.token"
|
|
@authenticated="handleAuthenticated"
|
|
/>
|
|
|
|
<main v-else-if="adminSessionState.ready && !restoreError" class="app-shell">
|
|
<aside class="sidebar">
|
|
<div class="brand">
|
|
<div class="brand-mark">棋</div>
|
|
<div>
|
|
<h1>自助棋牌室</h1>
|
|
<p>运营后台</p>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="nav-list" aria-label="后台模块">
|
|
<RouterLink
|
|
v-for="item in visibleNavigation"
|
|
:key="item.menuKey"
|
|
:to="item.path"
|
|
class="nav-item"
|
|
active-class="active"
|
|
>
|
|
<component :is="navigationIcons[item.icon]" :size="18" />
|
|
<span>{{ item.label }}</span>
|
|
</RouterLink>
|
|
</nav>
|
|
</aside>
|
|
|
|
<section class="workspace">
|
|
<header class="topbar">
|
|
<div>
|
|
<p class="eyebrow">{{ route.meta.stage || 'M08-D' }}</p>
|
|
<h2>{{ route.meta.title || '运营后台' }}</h2>
|
|
</div>
|
|
<div class="session-box">
|
|
<div class="session-user">
|
|
<span>{{ adminSessionState.user?.nickname?.slice(0, 1) || '管' }}</span>
|
|
<div>
|
|
<strong>{{ adminSessionState.user?.nickname || '管理员' }}</strong>
|
|
<small>{{ adminSessionState.access.roles.join(' · ') || '已登录' }}</small>
|
|
</div>
|
|
</div>
|
|
<el-tooltip content="安全退出" placement="bottom">
|
|
<el-button :icon="LogOut" circle @click="handleLogout" />
|
|
</el-tooltip>
|
|
</div>
|
|
</header>
|
|
|
|
<el-alert
|
|
v-if="routeLoadError"
|
|
class="route-alert"
|
|
type="error"
|
|
show-icon
|
|
:closable="false"
|
|
title="页面资源加载失败"
|
|
:description="routeLoadError"
|
|
>
|
|
<template #default>
|
|
<el-button type="primary" plain @click="retryLazyRoute">重新加载</el-button>
|
|
</template>
|
|
</el-alert>
|
|
|
|
<RouterView v-else v-slot="{ Component }">
|
|
<Suspense>
|
|
<component
|
|
:is="Component"
|
|
:session="session"
|
|
@open-cleaning="openCleaning"
|
|
/>
|
|
<template #fallback>
|
|
<section class="route-state" aria-live="polite">
|
|
<el-skeleton :rows="8" animated />
|
|
</section>
|
|
</template>
|
|
</Suspense>
|
|
</RouterView>
|
|
</section>
|
|
</main>
|
|
|
|
<main v-else class="session-recovery" aria-live="polite">
|
|
<section v-if="restoreError" class="session-recovery-card">
|
|
<h1>暂时无法恢复后台会话</h1>
|
|
<p>{{ restoreError }}</p>
|
|
<div>
|
|
<el-button type="primary" :loading="restoring" @click="restoreSession">重试</el-button>
|
|
<el-button @click="clearSession">返回登录</el-button>
|
|
</div>
|
|
</section>
|
|
<section v-else class="session-recovery-card">
|
|
<el-skeleton :rows="4" animated />
|
|
<p>正在安全恢复会话与权限菜单…</p>
|
|
</section>
|
|
</main>
|
|
</el-config-provider>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted, ref, type Component } from 'vue';
|
|
import { ElMessage } from 'element-plus';
|
|
import {
|
|
ClipboardList,
|
|
Handshake,
|
|
Images,
|
|
LayoutDashboard,
|
|
LogOut,
|
|
PanelsTopLeft,
|
|
RadioTower,
|
|
ScrollText,
|
|
Sparkles,
|
|
Store,
|
|
TicketCheck,
|
|
UsersRound,
|
|
WalletCards
|
|
} from '@lucide/vue';
|
|
import { useRoute } from 'vue-router';
|
|
import AdminLoginPanel from './components/AdminLoginPanel.vue';
|
|
import {
|
|
clearStoredSession,
|
|
getAdminMe,
|
|
logoutAdmin,
|
|
storeAdminSession,
|
|
type ApiSession
|
|
} from './api';
|
|
import {
|
|
ADMIN_NAVIGATION,
|
|
firstAuthorizedPath,
|
|
isNavigationAllowed,
|
|
resolveAuthorizedPath,
|
|
type AdminMenuKey
|
|
} from './navigation.mjs';
|
|
import { router } from './router';
|
|
import {
|
|
adminSessionState,
|
|
applyAdminIdentity,
|
|
applyAdminSession,
|
|
clearAdminSessionState
|
|
} from './session';
|
|
import type { AdminSessionPayload } from './types';
|
|
|
|
const route = useRoute();
|
|
const restoring = ref(false);
|
|
const restoreError = ref('');
|
|
const routeLoadError = ref('');
|
|
const session = computed<ApiSession>(() => ({ token: adminSessionState.token }));
|
|
const visibleNavigation = computed(() => ADMIN_NAVIGATION.filter(
|
|
(item) => isNavigationAllowed(adminSessionState.access, item)
|
|
));
|
|
const navigationIcons: Record<string, Component> = {
|
|
ClipboardList,
|
|
Handshake,
|
|
Images,
|
|
LayoutDashboard,
|
|
PanelsTopLeft,
|
|
RadioTower,
|
|
ScrollText,
|
|
Sparkles,
|
|
Store,
|
|
TicketCheck,
|
|
UsersRound,
|
|
WalletCards
|
|
};
|
|
|
|
function handleAuthenticated(payload: AdminSessionPayload) {
|
|
storeAdminSession(payload);
|
|
applyAdminSession(payload);
|
|
restoreError.value = '';
|
|
void ensureAllowedRoute();
|
|
ElMessage.success('登录成功');
|
|
}
|
|
|
|
async function handleLogout() {
|
|
try {
|
|
await logoutAdmin(session.value);
|
|
} catch {
|
|
// 本地令牌清理是退出动作的最终安全边界。
|
|
}
|
|
clearSession();
|
|
}
|
|
|
|
function clearSession() {
|
|
clearStoredSession();
|
|
clearAdminSessionState();
|
|
restoreError.value = '';
|
|
routeLoadError.value = '';
|
|
void router.replace('/overview');
|
|
}
|
|
|
|
function handleRefreshed(event: Event) {
|
|
applyAdminSession((event as CustomEvent<AdminSessionPayload>).detail);
|
|
void ensureAllowedRoute();
|
|
}
|
|
|
|
async function restoreSession() {
|
|
if (!adminSessionState.token || restoring.value) return;
|
|
restoring.value = true;
|
|
restoreError.value = '';
|
|
adminSessionState.ready = false;
|
|
try {
|
|
const result = await getAdminMe(session.value);
|
|
applyAdminIdentity(result.user, result.access);
|
|
await ensureAllowedRoute();
|
|
} catch (error) {
|
|
if (!localStorage.getItem('qipai.admin.token')) {
|
|
clearAdminSessionState();
|
|
} else {
|
|
restoreError.value = error instanceof Error
|
|
? `${error.message}。请检查网络后重试,或返回登录。`
|
|
: '会话恢复失败,请检查网络后重试。';
|
|
adminSessionState.ready = true;
|
|
}
|
|
} finally {
|
|
restoring.value = false;
|
|
}
|
|
}
|
|
|
|
async function ensureAllowedRoute() {
|
|
const menuKey = route.meta.menuKey as AdminMenuKey | undefined;
|
|
const target = menuKey
|
|
? resolveAuthorizedPath(adminSessionState.access, menuKey)
|
|
: firstAuthorizedPath(adminSessionState.access);
|
|
if (target && target !== route.path) await router.replace(target);
|
|
}
|
|
|
|
function openCleaning() {
|
|
void router.push('/cleaning');
|
|
}
|
|
|
|
function retryLazyRoute() {
|
|
window.location.reload();
|
|
}
|
|
|
|
const removeRouterErrorHandler = router.onError((error) => {
|
|
routeLoadError.value = error instanceof Error
|
|
? `${error.message}。可重新加载页面恢复。`
|
|
: '路由资源加载失败,可重新加载页面恢复。';
|
|
});
|
|
const removeAfterEach = router.afterEach((_to, _from, failure) => {
|
|
if (!failure) routeLoadError.value = '';
|
|
});
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('qipai:admin-refreshed', handleRefreshed);
|
|
window.addEventListener('qipai:admin-unauthorized', clearSession);
|
|
if (adminSessionState.token) void restoreSession();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('qipai:admin-refreshed', handleRefreshed);
|
|
window.removeEventListener('qipai:admin-unauthorized', clearSession);
|
|
removeRouterErrorHandler();
|
|
removeAfterEach();
|
|
});
|
|
</script>
|