Remove client-side token storage and Authorization header injection Backend login now only returns user data, omitting the session token Remove Authorization from backend CORS allowed headers Clean up obsolete VITE_* environment variable fallbacks Update Modal component to use Vue useId() instead of Math.random()
186 lines
4.9 KiB
Vue
186 lines
4.9 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, notifyAuthChange, onAuthChange, 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;
|
|
}
|
|
}
|
|
|
|
async function logout() {
|
|
try {
|
|
await api.logout();
|
|
} catch {
|
|
// The local session is cleared even when the server session is already gone.
|
|
}
|
|
|
|
currentUser.value = null;
|
|
notifyAuthChange();
|
|
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 = onAuthChange(() => {
|
|
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>
|