perf(pokemon): cache fetch options locally to reduce API requests

Add `all` parameter to fetch-options API to retrieve the full list.
Fetch all options once and filter locally in the Pokemon edit view to improve search responsiveness.
This commit is contained in:
2026-05-03 22:34:49 +08:00
parent df212a4e27
commit a0e07f101a
4 changed files with 63 additions and 9 deletions

View File

@@ -52,6 +52,9 @@ const message = ref('');
const fetchInput = ref<HTMLInputElement | null>(null);
const fetchIdentifier = ref('');
const fetchOptions = ref<PokemonFetchOption[]>([]);
const allFetchOptions = ref<PokemonFetchOption[]>([]);
const fetchOptionsLoaded = ref(false);
const fetchOptionsFailed = ref(false);
const fetchResultsStyle = ref<CSSProperties>({});
const imageOptions = ref<PokemonImage[]>([]);
const currentPokemonImage = ref<PokemonImage | null>(null);
@@ -62,6 +65,7 @@ const heightUnit = ref<'imperial' | 'metric'>('imperial');
const weightUnit = ref<'imperial' | 'metric'>('imperial');
let fetchOptionsController: AbortController | null = null;
let fetchPositionFrame = 0;
const fetchOptionsLimit = 20;
function defaultPokemonStats(): PokemonStats {
return {
@@ -328,24 +332,57 @@ function cancelFetchOptionsRequest() {
fetchOptionsLoading.value = false;
}
function resetFetchOptionsCache() {
cancelFetchOptionsRequest();
allFetchOptions.value = [];
fetchOptions.value = [];
fetchOptionsLoaded.value = false;
fetchOptionsFailed.value = false;
}
function fetchOptionLabel(option: PokemonFetchOption) {
return `#${option.id} ${option.name}`;
}
function fetchOptionMatchesSearch(option: PokemonFetchOption, search: string) {
if (!search) {
return true;
}
const keyword = search.toLowerCase();
return [String(option.id), option.identifier, option.name].some((value) => value.toLowerCase().includes(keyword));
}
function applyLocalFetchOptions() {
const search = fetchIdentifier.value.trim();
fetchOptions.value = allFetchOptions.value.filter((option) => fetchOptionMatchesSearch(option, search)).slice(0, fetchOptionsLimit);
}
async function loadFetchOptions() {
if (!canFetchPokemon.value) {
return;
}
if (fetchOptionsLoaded.value || fetchOptionsFailed.value) {
applyLocalFetchOptions();
return;
}
if (fetchOptionsController) {
return;
}
cancelFetchOptionsRequest();
const controller = new AbortController();
fetchOptionsController = controller;
fetchOptionsLoading.value = true;
try {
const rows = await api.pokemonFetchOptions(fetchIdentifier.value, controller.signal);
const rows = await api.pokemonFetchOptions('', controller.signal, true);
if (fetchOptionsController === controller) {
fetchOptions.value = rows;
allFetchOptions.value = rows;
fetchOptionsLoaded.value = true;
applyLocalFetchOptions();
}
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
@@ -353,6 +390,7 @@ async function loadFetchOptions() {
}
if (fetchOptionsController === controller) {
fetchOptions.value = [];
fetchOptionsFailed.value = true;
message.value = errorText(error, t('pages.pokemon.fetchSearchFailed'));
}
} finally {
@@ -368,6 +406,11 @@ function refreshFetchOptions() {
return;
}
if (fetchOptionsLoaded.value || fetchOptionsFailed.value) {
applyLocalFetchOptions();
return;
}
void loadFetchOptions();
}
@@ -446,6 +489,9 @@ function closeFetchOptions() {
fetchResultsStyle.value = {};
removeFetchPositionListeners();
cancelFetchOptionsRequest();
if (!fetchOptionsLoaded.value) {
fetchOptionsFailed.value = false;
}
}
function handleFetchIdentifierInput() {
@@ -670,6 +716,10 @@ onBeforeUnmount(() => {
watch(() => pokemonForm.value.skillIds.slice(), syncSkillItemDrops);
watch(fetchIdentifier, refreshFetchOptions);
watch(locale, () => {
resetFetchOptionsCache();
refreshFetchOptions();
});
</script>
<template>