From 70f7a73e6df816a89c01635f8793412644018a41 Mon Sep 17 00:00:00 2001 From: xiaomai Date: Wed, 6 May 2026 15:59:36 +0800 Subject: [PATCH] fix(frontend): safely resolve route IDs and remove manual auth checks Prevent invalid API calls during route transitions in detail views Allow builds for esbuild and @parcel/watcher in pnpm workspace --- frontend/src/views/HabitatDetail.vue | 44 +++++++++++++++++++++------- frontend/src/views/HabitatEdit.vue | 6 ---- frontend/src/views/ItemDetail.vue | 40 ++++++++++++++++++------- frontend/src/views/ItemEdit.vue | 6 ---- frontend/src/views/PokemonDetail.vue | 44 +++++++++++++++++++++------- frontend/src/views/PokemonEdit.vue | 6 ---- pnpm-workspace.yaml | 3 ++ 7 files changed, 101 insertions(+), 48 deletions(-) diff --git a/frontend/src/views/HabitatDetail.vue b/frontend/src/views/HabitatDetail.vue index 7475d69..4150572 100644 --- a/frontend/src/views/HabitatDetail.vue +++ b/frontend/src/views/HabitatDetail.vue @@ -13,7 +13,7 @@ import Skeleton from '../components/Skeleton.vue'; import Tabs, { type TabOption } from '../components/Tabs.vue'; import { iconBack, iconEdit, iconHabitat } from '../icons'; import { applySeo, resolvedSeoHead, resolveSeo } from '../seo'; -import { api, getAuthToken, type AuthUser, type HabitatDetail } from '../services/api'; +import { api, type AuthUser, type HabitatDetail } from '../services/api'; import HabitatEdit from './HabitatEdit.vue'; const route = useRoute(); @@ -23,6 +23,7 @@ const currentUser = ref(null); const detailTab = ref('details'); const timeOfDays = ['早晨', '中午', '傍晚', '晚上']; const weathers = ['晴天', '阴天', '雨天']; +const habitatDetailRouteNames = new Set(['habitat-detail', 'habitat-edit']); const showEditor = computed(() => route.name === 'habitat-edit'); const canUpdateHabitat = computed(() => currentUser.value?.permissions.includes('habitats.update') === true); const listPath = computed(() => (habitat.value?.isEventItem ? '/event-habitats' : '/habitats')); @@ -34,10 +35,15 @@ const detailTabs = computed(() => [ ]); const { data: initialHabitat } = await useAsyncData( - `habitat-detail:${String(route.params.id)}:${locale.value}`, + `habitat-detail:${activeHabitatRouteId() ?? 'none'}:${locale.value}`, async () => { + const routeId = activeHabitatRouteId(); + if (!routeId) { + return null; + } + try { - return await api.habitatDetail(String(route.params.id)); + return await api.habitatDetail(routeId); } catch { return null; } @@ -100,6 +106,15 @@ function weatherLabel(value: string): string { return labels[value] ?? value; } +function activeHabitatRouteId(): string | null { + return typeof route.name === 'string' && + habitatDetailRouteNames.has(route.name) && + typeof route.params.id === 'string' && + route.params.id.trim() !== '' + ? route.params.id + : null; +} + const pokemonRows = computed(() => { if (!habitat.value) return []; @@ -146,8 +161,14 @@ const pokemonRows = computed(() => { }); async function loadHabitatDetail() { + const routeId = activeHabitatRouteId(); + if (!routeId) { + initialHabitatLoaded.value = true; + return; + } + try { - const nextHabitat = await api.habitatDetail(String(route.params.id)); + const nextHabitat = await api.habitatDetail(routeId); habitat.value = nextHabitat; initialHabitatLoaded.value = true; @@ -166,13 +187,12 @@ async function loadHabitatDetail() { } onMounted(async () => { - if (getAuthToken()) { - try { - currentUser.value = (await api.me()).user; - } catch { - currentUser.value = null; - } + try { + currentUser.value = (await api.me()).user; + } catch { + currentUser.value = null; } + if (!initialHabitatLoaded.value) { await loadHabitatDetail(); } @@ -190,6 +210,10 @@ watch( watch( () => route.params.id, () => { + if (!activeHabitatRouteId()) { + return; + } + habitat.value = null; detailTab.value = 'details'; void loadHabitatDetail(); diff --git a/frontend/src/views/HabitatEdit.vue b/frontend/src/views/HabitatEdit.vue index c50d470..88ab458 100644 --- a/frontend/src/views/HabitatEdit.vue +++ b/frontend/src/views/HabitatEdit.vue @@ -13,7 +13,6 @@ import TranslationFields from '../components/TranslationFields.vue'; import { iconAdd, iconCancel, iconDelete, iconPokemon, iconSave } from '../icons'; import { api, - getAuthToken, type AuthUser, type ConfigType, type EntityImage, @@ -156,11 +155,6 @@ function habitatNameForSave() { } async function loadCurrentUser() { - if (!getAuthToken()) { - currentUser.value = null; - return; - } - try { currentUser.value = (await api.me()).user; } catch { diff --git a/frontend/src/views/ItemDetail.vue b/frontend/src/views/ItemDetail.vue index 2d0da08..790721c 100644 --- a/frontend/src/views/ItemDetail.vue +++ b/frontend/src/views/ItemDetail.vue @@ -13,7 +13,7 @@ import Skeleton from '../components/Skeleton.vue'; import Tabs, { type TabOption } from '../components/Tabs.vue'; import { iconAdd, iconBack, iconEdit, iconHabitat, iconItem } from '../icons'; import { applySeo, resolvedSeoHead, resolveSeo } from '../seo'; -import { api, getAuthToken, type AuthUser, type ItemDetail } from '../services/api'; +import { api, type AuthUser, type ItemDetail } from '../services/api'; import ItemEdit from './ItemEdit.vue'; const route = useRoute(); @@ -74,10 +74,15 @@ const possibleTagEvidenceSections = computed(() => [ ]); const { data: initialItem } = await useAsyncData( - `item-detail:${String(route.name)}:${String(route.params.id)}:${locale.value}`, + `item-detail:${String(route.name)}:${activeItemRouteId() ?? 'none'}:${locale.value}`, async () => { + const routeId = activeItemRouteId(); + if (!routeId) { + return null; + } + try { - const nextItem = await api.itemDetail(String(route.params.id)); + const nextItem = await api.itemDetail(routeId); return isAncientArtifactRoute.value && !nextItem.ancientArtifactCategory ? null : nextItem; } catch { return null; @@ -114,8 +119,14 @@ const customization = computed(() => { }); async function loadItemDetail() { + const routeId = activeItemRouteId(); + if (!routeId) { + initialItemLoaded.value = true; + return; + } + try { - const nextItem = await api.itemDetail(String(route.params.id)); + const nextItem = await api.itemDetail(routeId); if (isAncientArtifactRoute.value && !nextItem.ancientArtifactCategory) { await router.replace(route.name === 'ancient-artifact-edit' ? `/items/${nextItem.id}/edit` : `/items/${nextItem.id}`); @@ -143,14 +154,19 @@ function isItemDetailRouteName(value: unknown) { return typeof value === 'string' && itemDetailRouteNames.has(value); } +function activeItemRouteId(): string | null { + return isItemDetailRouteName(route.name) && typeof route.params.id === 'string' && route.params.id.trim() !== '' + ? route.params.id + : null; +} + onMounted(async () => { - if (getAuthToken()) { - try { - currentUser.value = (await api.me()).user; - } catch { - currentUser.value = null; - } + try { + currentUser.value = (await api.me()).user; + } catch { + currentUser.value = null; } + if (!initialItemLoaded.value) { await loadItemDetail(); } @@ -170,6 +186,10 @@ watch( watch( () => route.params.id, () => { + if (!activeItemRouteId()) { + return; + } + item.value = null; detailTab.value = 'details'; void loadItemDetail(); diff --git a/frontend/src/views/ItemEdit.vue b/frontend/src/views/ItemEdit.vue index 52af56a..a5ed98c 100644 --- a/frontend/src/views/ItemEdit.vue +++ b/frontend/src/views/ItemEdit.vue @@ -12,7 +12,6 @@ import TranslationFields from '../components/TranslationFields.vue'; import { iconCancel, iconSave } from '../icons'; import { api, - getAuthToken, type AuthUser, type ConfigType, type EntityImage, @@ -215,11 +214,6 @@ async function loadOptions() { } async function loadCurrentUser() { - if (!getAuthToken()) { - currentUser.value = null; - return; - } - try { currentUser.value = (await api.me()).user; } catch { diff --git a/frontend/src/views/PokemonDetail.vue b/frontend/src/views/PokemonDetail.vue index a552613..94d7ff4 100644 --- a/frontend/src/views/PokemonDetail.vue +++ b/frontend/src/views/PokemonDetail.vue @@ -16,7 +16,7 @@ import StatusMessage from '../components/StatusMessage.vue'; import Tabs, { type TabOption } from '../components/Tabs.vue'; import { iconAdd, iconBack, iconCancel, iconCheck, iconEdit, iconHabitat, iconItem } from '../icons'; import { applySeo, resolvedSeoHead, resolveSeo } from '../seo'; -import { api, getAuthToken, type AuthUser, type Item, type PokemonDetail, type PokemonPayload, type TradingPreference } from '../services/api'; +import { api, type AuthUser, type Item, type PokemonDetail, type PokemonPayload, type TradingPreference } from '../services/api'; import PokemonEdit from './PokemonEdit.vue'; const route = useRoute(); @@ -39,12 +39,18 @@ const tradingDraftItems = ref( - `pokemon-detail:${String(route.params.id)}:${locale.value}`, + `pokemon-detail:${activePokemonRouteId() ?? 'none'}:${locale.value}`, async () => { + const routeId = activePokemonRouteId(); + if (!routeId) { + return null; + } + try { - return await api.pokemonDetail(String(route.params.id)); + return await api.pokemonDetail(routeId); } catch { return null; } @@ -93,6 +99,15 @@ function habitatTabValue(id: number): string { return `habitat-${id}`; } +function activePokemonRouteId(): string | null { + return typeof route.name === 'string' && + pokemonDetailRouteNames.has(route.name) && + typeof route.params.id === 'string' && + route.params.id.trim() !== '' + ? route.params.id + : null; +} + function timeLabel(value: string): string { const labels: Record = { 早晨: t('appearance.morning'), @@ -439,8 +454,14 @@ async function saveTradingItems() { } async function loadPokemonDetail() { + const routeId = activePokemonRouteId(); + if (!routeId) { + initialPokemonLoaded.value = true; + return; + } + try { - const nextPokemon = await api.pokemonDetail(String(route.params.id)); + const nextPokemon = await api.pokemonDetail(routeId); pokemon.value = nextPokemon; relatedHabitatTab.value = habitatTabValue(nextPokemon.environment.id); initialPokemonLoaded.value = true; @@ -461,13 +482,12 @@ async function loadPokemonDetail() { } onMounted(async () => { - if (getAuthToken()) { - try { - currentUser.value = (await api.me()).user; - } catch { - currentUser.value = null; - } + try { + currentUser.value = (await api.me()).user; + } catch { + currentUser.value = null; } + if (!initialPokemonLoaded.value) { await loadPokemonDetail(); } @@ -485,6 +505,10 @@ watch( watch( () => route.params.id, () => { + if (!activePokemonRouteId()) { + return; + } + pokemon.value = null; relatedHabitatTab.value = ''; detailTab.value = 'details'; diff --git a/frontend/src/views/PokemonEdit.vue b/frontend/src/views/PokemonEdit.vue index e9ad3fa..7117e55 100644 --- a/frontend/src/views/PokemonEdit.vue +++ b/frontend/src/views/PokemonEdit.vue @@ -14,7 +14,6 @@ import TranslationFields from '../components/TranslationFields.vue'; import { iconCancel, iconSave, iconSearch } from '../icons'; import { api, - getAuthToken, type AuthUser, type ConfigType, type EntityImage, @@ -195,11 +194,6 @@ async function loadOptions() { } async function loadCurrentUser() { - if (!getAuthToken()) { - currentUser.value = null; - return; - } - try { currentUser.value = (await api.me()).user; } catch { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 507e28e..c4b02ba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,6 @@ packages: - backend - frontend +allowBuilds: + '@parcel/watcher': true + esbuild: true