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
This commit is contained in:
@@ -13,7 +13,7 @@ import Skeleton from '../components/Skeleton.vue';
|
|||||||
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
||||||
import { iconBack, iconEdit, iconHabitat } from '../icons';
|
import { iconBack, iconEdit, iconHabitat } from '../icons';
|
||||||
import { applySeo, resolvedSeoHead, resolveSeo } from '../seo';
|
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';
|
import HabitatEdit from './HabitatEdit.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -23,6 +23,7 @@ const currentUser = ref<AuthUser | null>(null);
|
|||||||
const detailTab = ref('details');
|
const detailTab = ref('details');
|
||||||
const timeOfDays = ['早晨', '中午', '傍晚', '晚上'];
|
const timeOfDays = ['早晨', '中午', '傍晚', '晚上'];
|
||||||
const weathers = ['晴天', '阴天', '雨天'];
|
const weathers = ['晴天', '阴天', '雨天'];
|
||||||
|
const habitatDetailRouteNames = new Set(['habitat-detail', 'habitat-edit']);
|
||||||
const showEditor = computed(() => route.name === 'habitat-edit');
|
const showEditor = computed(() => route.name === 'habitat-edit');
|
||||||
const canUpdateHabitat = computed(() => currentUser.value?.permissions.includes('habitats.update') === true);
|
const canUpdateHabitat = computed(() => currentUser.value?.permissions.includes('habitats.update') === true);
|
||||||
const listPath = computed(() => (habitat.value?.isEventItem ? '/event-habitats' : '/habitats'));
|
const listPath = computed(() => (habitat.value?.isEventItem ? '/event-habitats' : '/habitats'));
|
||||||
@@ -34,10 +35,15 @@ const detailTabs = computed<TabOption[]>(() => [
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const { data: initialHabitat } = await useAsyncData<HabitatDetail | null>(
|
const { data: initialHabitat } = await useAsyncData<HabitatDetail | null>(
|
||||||
`habitat-detail:${String(route.params.id)}:${locale.value}`,
|
`habitat-detail:${activeHabitatRouteId() ?? 'none'}:${locale.value}`,
|
||||||
async () => {
|
async () => {
|
||||||
|
const routeId = activeHabitatRouteId();
|
||||||
|
if (!routeId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await api.habitatDetail(String(route.params.id));
|
return await api.habitatDetail(routeId);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -100,6 +106,15 @@ function weatherLabel(value: string): string {
|
|||||||
return labels[value] ?? value;
|
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<PokemonRow[]>(() => {
|
const pokemonRows = computed<PokemonRow[]>(() => {
|
||||||
if (!habitat.value) return [];
|
if (!habitat.value) return [];
|
||||||
|
|
||||||
@@ -146,8 +161,14 @@ const pokemonRows = computed<PokemonRow[]>(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadHabitatDetail() {
|
async function loadHabitatDetail() {
|
||||||
|
const routeId = activeHabitatRouteId();
|
||||||
|
if (!routeId) {
|
||||||
|
initialHabitatLoaded.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nextHabitat = await api.habitatDetail(String(route.params.id));
|
const nextHabitat = await api.habitatDetail(routeId);
|
||||||
habitat.value = nextHabitat;
|
habitat.value = nextHabitat;
|
||||||
initialHabitatLoaded.value = true;
|
initialHabitatLoaded.value = true;
|
||||||
|
|
||||||
@@ -166,13 +187,12 @@ async function loadHabitatDetail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (getAuthToken()) {
|
try {
|
||||||
try {
|
currentUser.value = (await api.me()).user;
|
||||||
currentUser.value = (await api.me()).user;
|
} catch {
|
||||||
} catch {
|
currentUser.value = null;
|
||||||
currentUser.value = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!initialHabitatLoaded.value) {
|
if (!initialHabitatLoaded.value) {
|
||||||
await loadHabitatDetail();
|
await loadHabitatDetail();
|
||||||
}
|
}
|
||||||
@@ -190,6 +210,10 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => route.params.id,
|
() => route.params.id,
|
||||||
() => {
|
() => {
|
||||||
|
if (!activeHabitatRouteId()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
habitat.value = null;
|
habitat.value = null;
|
||||||
detailTab.value = 'details';
|
detailTab.value = 'details';
|
||||||
void loadHabitatDetail();
|
void loadHabitatDetail();
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import TranslationFields from '../components/TranslationFields.vue';
|
|||||||
import { iconAdd, iconCancel, iconDelete, iconPokemon, iconSave } from '../icons';
|
import { iconAdd, iconCancel, iconDelete, iconPokemon, iconSave } from '../icons';
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
getAuthToken,
|
|
||||||
type AuthUser,
|
type AuthUser,
|
||||||
type ConfigType,
|
type ConfigType,
|
||||||
type EntityImage,
|
type EntityImage,
|
||||||
@@ -156,11 +155,6 @@ function habitatNameForSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadCurrentUser() {
|
async function loadCurrentUser() {
|
||||||
if (!getAuthToken()) {
|
|
||||||
currentUser.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
currentUser.value = (await api.me()).user;
|
currentUser.value = (await api.me()).user;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Skeleton from '../components/Skeleton.vue';
|
|||||||
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
||||||
import { iconAdd, iconBack, iconEdit, iconHabitat, iconItem } from '../icons';
|
import { iconAdd, iconBack, iconEdit, iconHabitat, iconItem } from '../icons';
|
||||||
import { applySeo, resolvedSeoHead, resolveSeo } from '../seo';
|
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';
|
import ItemEdit from './ItemEdit.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -74,10 +74,15 @@ const possibleTagEvidenceSections = computed(() => [
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const { data: initialItem } = await useAsyncData<ItemDetail | null>(
|
const { data: initialItem } = await useAsyncData<ItemDetail | null>(
|
||||||
`item-detail:${String(route.name)}:${String(route.params.id)}:${locale.value}`,
|
`item-detail:${String(route.name)}:${activeItemRouteId() ?? 'none'}:${locale.value}`,
|
||||||
async () => {
|
async () => {
|
||||||
|
const routeId = activeItemRouteId();
|
||||||
|
if (!routeId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nextItem = await api.itemDetail(String(route.params.id));
|
const nextItem = await api.itemDetail(routeId);
|
||||||
return isAncientArtifactRoute.value && !nextItem.ancientArtifactCategory ? null : nextItem;
|
return isAncientArtifactRoute.value && !nextItem.ancientArtifactCategory ? null : nextItem;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -114,8 +119,14 @@ const customization = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadItemDetail() {
|
async function loadItemDetail() {
|
||||||
|
const routeId = activeItemRouteId();
|
||||||
|
if (!routeId) {
|
||||||
|
initialItemLoaded.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nextItem = await api.itemDetail(String(route.params.id));
|
const nextItem = await api.itemDetail(routeId);
|
||||||
|
|
||||||
if (isAncientArtifactRoute.value && !nextItem.ancientArtifactCategory) {
|
if (isAncientArtifactRoute.value && !nextItem.ancientArtifactCategory) {
|
||||||
await router.replace(route.name === 'ancient-artifact-edit' ? `/items/${nextItem.id}/edit` : `/items/${nextItem.id}`);
|
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);
|
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 () => {
|
onMounted(async () => {
|
||||||
if (getAuthToken()) {
|
try {
|
||||||
try {
|
currentUser.value = (await api.me()).user;
|
||||||
currentUser.value = (await api.me()).user;
|
} catch {
|
||||||
} catch {
|
currentUser.value = null;
|
||||||
currentUser.value = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!initialItemLoaded.value) {
|
if (!initialItemLoaded.value) {
|
||||||
await loadItemDetail();
|
await loadItemDetail();
|
||||||
}
|
}
|
||||||
@@ -170,6 +186,10 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => route.params.id,
|
() => route.params.id,
|
||||||
() => {
|
() => {
|
||||||
|
if (!activeItemRouteId()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
item.value = null;
|
item.value = null;
|
||||||
detailTab.value = 'details';
|
detailTab.value = 'details';
|
||||||
void loadItemDetail();
|
void loadItemDetail();
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import TranslationFields from '../components/TranslationFields.vue';
|
|||||||
import { iconCancel, iconSave } from '../icons';
|
import { iconCancel, iconSave } from '../icons';
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
getAuthToken,
|
|
||||||
type AuthUser,
|
type AuthUser,
|
||||||
type ConfigType,
|
type ConfigType,
|
||||||
type EntityImage,
|
type EntityImage,
|
||||||
@@ -215,11 +214,6 @@ async function loadOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadCurrentUser() {
|
async function loadCurrentUser() {
|
||||||
if (!getAuthToken()) {
|
|
||||||
currentUser.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
currentUser.value = (await api.me()).user;
|
currentUser.value = (await api.me()).user;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import StatusMessage from '../components/StatusMessage.vue';
|
|||||||
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
||||||
import { iconAdd, iconBack, iconCancel, iconCheck, iconEdit, iconHabitat, iconItem } from '../icons';
|
import { iconAdd, iconBack, iconCancel, iconCheck, iconEdit, iconHabitat, iconItem } from '../icons';
|
||||||
import { applySeo, resolvedSeoHead, resolveSeo } from '../seo';
|
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';
|
import PokemonEdit from './PokemonEdit.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -39,12 +39,18 @@ const tradingDraftItems = ref<Array<{ itemId: number; preference: TradingPrefere
|
|||||||
const timeOfDays = ['早晨', '中午', '傍晚', '晚上'];
|
const timeOfDays = ['早晨', '中午', '傍晚', '晚上'];
|
||||||
const weathers = ['晴天', '阴天', '雨天'];
|
const weathers = ['晴天', '阴天', '雨天'];
|
||||||
const relatedPokemonLimit = 6;
|
const relatedPokemonLimit = 6;
|
||||||
|
const pokemonDetailRouteNames = new Set(['pokemon-detail', 'pokemon-edit']);
|
||||||
|
|
||||||
const { data: initialPokemon } = await useAsyncData<PokemonDetail | null>(
|
const { data: initialPokemon } = await useAsyncData<PokemonDetail | null>(
|
||||||
`pokemon-detail:${String(route.params.id)}:${locale.value}`,
|
`pokemon-detail:${activePokemonRouteId() ?? 'none'}:${locale.value}`,
|
||||||
async () => {
|
async () => {
|
||||||
|
const routeId = activePokemonRouteId();
|
||||||
|
if (!routeId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await api.pokemonDetail(String(route.params.id));
|
return await api.pokemonDetail(routeId);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -93,6 +99,15 @@ function habitatTabValue(id: number): string {
|
|||||||
return `habitat-${id}`;
|
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 {
|
function timeLabel(value: string): string {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
早晨: t('appearance.morning'),
|
早晨: t('appearance.morning'),
|
||||||
@@ -439,8 +454,14 @@ async function saveTradingItems() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadPokemonDetail() {
|
async function loadPokemonDetail() {
|
||||||
|
const routeId = activePokemonRouteId();
|
||||||
|
if (!routeId) {
|
||||||
|
initialPokemonLoaded.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nextPokemon = await api.pokemonDetail(String(route.params.id));
|
const nextPokemon = await api.pokemonDetail(routeId);
|
||||||
pokemon.value = nextPokemon;
|
pokemon.value = nextPokemon;
|
||||||
relatedHabitatTab.value = habitatTabValue(nextPokemon.environment.id);
|
relatedHabitatTab.value = habitatTabValue(nextPokemon.environment.id);
|
||||||
initialPokemonLoaded.value = true;
|
initialPokemonLoaded.value = true;
|
||||||
@@ -461,13 +482,12 @@ async function loadPokemonDetail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (getAuthToken()) {
|
try {
|
||||||
try {
|
currentUser.value = (await api.me()).user;
|
||||||
currentUser.value = (await api.me()).user;
|
} catch {
|
||||||
} catch {
|
currentUser.value = null;
|
||||||
currentUser.value = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!initialPokemonLoaded.value) {
|
if (!initialPokemonLoaded.value) {
|
||||||
await loadPokemonDetail();
|
await loadPokemonDetail();
|
||||||
}
|
}
|
||||||
@@ -485,6 +505,10 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => route.params.id,
|
() => route.params.id,
|
||||||
() => {
|
() => {
|
||||||
|
if (!activePokemonRouteId()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pokemon.value = null;
|
pokemon.value = null;
|
||||||
relatedHabitatTab.value = '';
|
relatedHabitatTab.value = '';
|
||||||
detailTab.value = 'details';
|
detailTab.value = 'details';
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import TranslationFields from '../components/TranslationFields.vue';
|
|||||||
import { iconCancel, iconSave, iconSearch } from '../icons';
|
import { iconCancel, iconSave, iconSearch } from '../icons';
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
getAuthToken,
|
|
||||||
type AuthUser,
|
type AuthUser,
|
||||||
type ConfigType,
|
type ConfigType,
|
||||||
type EntityImage,
|
type EntityImage,
|
||||||
@@ -195,11 +194,6 @@ async function loadOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadCurrentUser() {
|
async function loadCurrentUser() {
|
||||||
if (!getAuthToken()) {
|
|
||||||
currentUser.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
currentUser.value = (await api.me()).user;
|
currentUser.value = (await api.me()).user;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
packages:
|
packages:
|
||||||
- backend
|
- backend
|
||||||
- frontend
|
- frontend
|
||||||
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
|
esbuild: true
|
||||||
|
|||||||
Reference in New Issue
Block a user