Add HTTP-only cookie session support to backend for SSR compatibility Update frontend fetch calls to include credentials Maintain legacy bearer token support for SPA transition
189 lines
5.0 KiB
Vue
189 lines
5.0 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import AppShell from './src/components/AppShell.vue';
|
|
import {
|
|
iconAction,
|
|
iconAdmin,
|
|
iconArtifact,
|
|
iconAutomation,
|
|
iconChecklist,
|
|
iconClothes,
|
|
iconDish,
|
|
iconDreamIsland,
|
|
iconEvent,
|
|
iconHabitat,
|
|
iconHome,
|
|
iconItem,
|
|
iconLife,
|
|
iconPokemon,
|
|
iconRecipe,
|
|
type AppIcon
|
|
} from './src/icons';
|
|
import { getCurrentLocale, loadSystemWordings, onLocaleChange, setCurrentLocale } from './src/i18n';
|
|
import { api, getAuthToken, onAuthTokenChange, setAuthToken, type AuthUser, type Language } from './src/services/api';
|
|
|
|
const { t, locale } = useI18n();
|
|
const router = useRouter();
|
|
const currentUser = ref<AuthUser | null>(null);
|
|
const languages = ref<Language[]>([
|
|
{ code: 'en', name: 'English', enabled: true, isDefault: true, sortOrder: 10 },
|
|
{ code: 'zh-CN', name: '简体中文', enabled: true, isDefault: false, sortOrder: 20 }
|
|
]);
|
|
let removeAuthListener: (() => void) | null = null;
|
|
let removeLocaleListener: (() => void) | null = null;
|
|
|
|
type NavBadge = {
|
|
label: string;
|
|
tone?: 'info' | 'success' | 'warning' | 'danger' | 'neutral';
|
|
};
|
|
|
|
type NavLinkItem = {
|
|
label: string;
|
|
to: string;
|
|
icon?: AppIcon;
|
|
badge?: NavBadge;
|
|
};
|
|
|
|
type NavGroupItem = {
|
|
key: string;
|
|
label: string;
|
|
icon?: AppIcon;
|
|
children: NavLinkItem[];
|
|
};
|
|
|
|
type NavItem = NavLinkItem | NavGroupItem;
|
|
|
|
function inDevBadge(): NavBadge {
|
|
return { label: t('common.inDev'), tone: 'info' };
|
|
}
|
|
|
|
function can(permissionKey: string) {
|
|
return currentUser.value?.permissions.includes(permissionKey) === true;
|
|
}
|
|
|
|
const navItems = computed<NavItem[]>(() => {
|
|
const items: NavItem[] = [
|
|
{ label: t('nav.home'), to: '/', icon: iconHome },
|
|
{
|
|
key: 'pokedex',
|
|
label: t('nav.pokedex'),
|
|
icon: iconPokemon,
|
|
children: [
|
|
{ label: t('nav.mainGame'), to: '/pokemon', icon: iconPokemon },
|
|
{ label: t('nav.event'), to: '/event-pokemon', icon: iconEvent }
|
|
]
|
|
},
|
|
{
|
|
key: 'habitat-dex',
|
|
label: t('nav.habitatDex'),
|
|
icon: iconHabitat,
|
|
children: [
|
|
{ label: t('nav.mainGame'), to: '/habitats', icon: iconHabitat },
|
|
{ label: t('nav.event'), to: '/event-habitats', icon: iconEvent }
|
|
]
|
|
},
|
|
{
|
|
key: 'collections',
|
|
label: t('nav.collections'),
|
|
icon: iconItem,
|
|
children: [
|
|
{ label: t('nav.mainGame'), to: '/items', icon: iconItem },
|
|
{ label: t('nav.event'), to: '/event-items', icon: iconEvent },
|
|
{ label: t('nav.ancientArtifacts'), to: '/ancient-artifacts', icon: iconArtifact }
|
|
]
|
|
},
|
|
{ label: t('nav.recipes'), to: '/recipes', icon: iconRecipe },
|
|
{ label: t('nav.automation'), to: '/automation', icon: iconAutomation, badge: inDevBadge() },
|
|
{ label: t('nav.dish'), to: '/dish', icon: iconDish },
|
|
{ label: t('nav.events'), to: '/events', icon: iconEvent, badge: inDevBadge() },
|
|
{ label: t('nav.actions'), to: '/actions', icon: iconAction, badge: inDevBadge() },
|
|
{ label: t('nav.dreamIsland'), to: '/dream-island', icon: iconDreamIsland, badge: inDevBadge() },
|
|
{ label: t('nav.clothes'), to: '/clothes', icon: iconClothes, badge: inDevBadge() },
|
|
{ label: t('nav.checklist'), to: '/checklist', icon: iconChecklist },
|
|
{ label: t('nav.life'), to: '/life', icon: iconLife }
|
|
];
|
|
|
|
if (can('admin.access')) {
|
|
items.push({ label: t('nav.admin'), to: '/admin', icon: iconAdmin });
|
|
}
|
|
|
|
return items;
|
|
});
|
|
|
|
async function loadCurrentUser() {
|
|
try {
|
|
const response = await api.me();
|
|
currentUser.value = response.user;
|
|
} catch {
|
|
currentUser.value = null;
|
|
if (getAuthToken()) {
|
|
setAuthToken(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function logout() {
|
|
try {
|
|
await api.logout();
|
|
} catch {
|
|
// The local session is cleared even when the server session is already gone.
|
|
}
|
|
|
|
currentUser.value = null;
|
|
setAuthToken(null);
|
|
await router.push('/');
|
|
}
|
|
|
|
async function loadLanguages() {
|
|
try {
|
|
const loadedLanguages = await api.languages();
|
|
if (loadedLanguages.length) {
|
|
languages.value = loadedLanguages;
|
|
}
|
|
|
|
if (!languages.value.some((language) => language.code === getCurrentLocale() && language.enabled)) {
|
|
setCurrentLocale('en');
|
|
}
|
|
|
|
await loadSystemWordings(getCurrentLocale());
|
|
} catch {
|
|
// Keep the built-in language list when the API is not ready yet.
|
|
}
|
|
}
|
|
|
|
async function updateLocale(value: string) {
|
|
await loadSystemWordings(value);
|
|
setCurrentLocale(value);
|
|
}
|
|
|
|
onMounted(() => {
|
|
void loadLanguages();
|
|
void loadCurrentUser();
|
|
removeAuthListener = onAuthTokenChange(() => {
|
|
void loadCurrentUser();
|
|
});
|
|
removeLocaleListener = onLocaleChange(() => {
|
|
void loadLanguages();
|
|
});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
removeAuthListener?.();
|
|
removeLocaleListener?.();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<AppShell
|
|
:current-user="currentUser"
|
|
:languages="languages"
|
|
:locale="locale"
|
|
:nav-items="navItems"
|
|
@logout="logout"
|
|
@update:locale="updateLocale"
|
|
>
|
|
<NuxtPage :key="locale" />
|
|
</AppShell>
|
|
</template>
|