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:
@@ -852,7 +852,7 @@ API 暴露边界:
|
||||
受权限保护的编辑 API:
|
||||
|
||||
- Pokemon、栖息地、物品、材料单的创建、更新、删除分别需要对应实体的 `create`、`update`、`delete` 权限。
|
||||
- `GET /api/pokemon/fetch-options`:按搜索词返回 Pokemon CSV data 搜索结果;需要 `pokemon.fetch`;只返回 `id`、`identifier`、`name`。
|
||||
- `GET /api/pokemon/fetch-options`:按搜索词返回 Pokemon CSV data 搜索结果;支持 `all=true` 返回完整候选列表供前端本地筛选;需要 `pokemon.fetch`;只返回 `id`、`identifier`、`name`。
|
||||
- `POST /api/pokemon/fetch`:按 data identifier 或 Pokemon ID 查询 CSV 资料并填充 Pokemon 编辑表单;需要 `pokemon.fetch`;不直接保存 Pokemon。
|
||||
- `POST /api/pokemon/image-options`:按 data identifier 或 Pokemon ID 查询 pokesprite 可用图片候选;需要 `pokemon.fetch`;只返回 `id`、`identifier` 和图片候选列表。
|
||||
- `POST /api/uploads/:entityType`:上传 Wiki 图片;需要对应实体上传权限;`entityType` 支持 `pokemon`、`items`、`habitats`;返回图片历史记录项和可展示 URL。
|
||||
|
||||
@@ -1804,12 +1804,13 @@ function pokemonFetchOptionMatches(
|
||||
|
||||
export async function listPokemonFetchOptions(paramsQuery: QueryParams, locale = defaultLocale): Promise<PokemonFetchOption[]> {
|
||||
const search = asString(paramsQuery.search)?.trim() ?? '';
|
||||
const includeAll = asString(paramsQuery.all) === 'true';
|
||||
const [data, languages] = await Promise.all([loadPokemonCsvData(), listLanguages()]);
|
||||
const rows = data.pokemonRows.filter(
|
||||
(row) => csvInteger(row, 'id') > 0 && pokemonFetchOptionMatches(row, data, languages, locale, search)
|
||||
);
|
||||
|
||||
return data.pokemonRows
|
||||
.filter((row) => csvInteger(row, 'id') > 0 && pokemonFetchOptionMatches(row, data, languages, locale, search))
|
||||
.slice(0, 20)
|
||||
.map((row) => pokemonFetchOption(row, data, languages, locale));
|
||||
return (includeAll ? rows : rows.slice(0, 20)).map((row) => pokemonFetchOption(row, data, languages, locale));
|
||||
}
|
||||
|
||||
function displayValue(value: string | null | undefined): string {
|
||||
|
||||
@@ -1008,8 +1008,11 @@ export const api = {
|
||||
pokemon: (params: Record<string, string | number | undefined>) =>
|
||||
getJson<Pokemon[]>(`/api/pokemon${buildQuery(params)}`),
|
||||
pokemonDetail: (id: string | number) => getJson<PokemonDetail>(`/api/pokemon/${id}`),
|
||||
pokemonFetchOptions: (search: string, signal?: AbortSignal) =>
|
||||
getJson<PokemonFetchOption[]>(`/api/pokemon/fetch-options${buildQuery({ search: search.trim() })}`, signal),
|
||||
pokemonFetchOptions: (search: string, signal?: AbortSignal, all = false) =>
|
||||
getJson<PokemonFetchOption[]>(
|
||||
`/api/pokemon/fetch-options${buildQuery({ search: search.trim(), all: all ? true : undefined })}`,
|
||||
signal
|
||||
),
|
||||
fetchPokemonData: (identifier: string) => sendJson<PokemonFetchResult>('/api/pokemon/fetch', 'POST', { identifier }),
|
||||
fetchPokemonImageOptions: (identifier: string) =>
|
||||
sendJson<PokemonImageOptionsResult>('/api/pokemon/image-options', 'POST', { identifier }),
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user