feat(ui): use modal dialogs for entity creation and editing

Introduce reusable Modal component for forms
Update router to preserve scroll position when toggling modals
Refactor admin and entity views to render editors as overlays
This commit is contained in:
2026-05-01 13:44:34 +08:00
parent bd068ce2f6
commit 6812ddc428
18 changed files with 717 additions and 172 deletions

View File

@@ -2,7 +2,7 @@
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import PageHeader from '../components/PageHeader.vue';
import Modal from '../components/Modal.vue';
import Skeleton from '../components/Skeleton.vue';
import StatusMessage from '../components/StatusMessage.vue';
import TagsSelect from '../components/TagsSelect.vue';
@@ -87,6 +87,10 @@ function skillDropLabel(skillId: string) {
return name ? t('pages.pokemon.skillDrop', { name }) : t('pages.pokemon.dropItem');
}
function closeEditor() {
void router.push(cancelTo.value);
}
async function loadEditor() {
loading.value = true;
message.value = '';
@@ -186,17 +190,10 @@ watch(() => pokemonForm.value.skillIds.slice(), syncSkillItemDrops);
</script>
<template>
<section class="page-stack">
<PageHeader :title="pageTitle" :subtitle="t('pages.pokemon.editSubtitle')">
<template #kicker>Pokédex Edit</template>
<template #actions>
<RouterLink class="ui-button ui-button--blue ui-button--small" :to="cancelTo">{{ t('common.back') }}</RouterLink>
</template>
</PageHeader>
<Modal :title="pageTitle" :subtitle="t('pages.pokemon.editSubtitle')" :close-label="t('common.close')" size="wide" @close="closeEditor">
<StatusMessage v-if="message" variant="danger">{{ message }}</StatusMessage>
<form v-if="!loading && options" class="detail-section" @submit.prevent="savePokemon">
<form v-if="!loading && options" id="pokemon-edit-form" class="modal-edit-form" @submit.prevent="savePokemon">
<div class="field">
<label for="pokemon-id">ID</label>
<input id="pokemon-id" v-model="pokemonForm.id" :disabled="isEditing" min="1" required type="number" />
@@ -271,18 +268,18 @@ watch(() => pokemonForm.value.skillIds.slice(), syncSkillItemDrops);
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="link-button" :disabled="busy">{{ busy ? t('common.saving') : t('common.save') }}</button>
<RouterLink class="plain-button" :to="cancelTo">{{ t('common.cancel') }}</RouterLink>
</div>
</form>
<section v-else class="detail-section skeleton-detail-section" aria-busy="true" :aria-label="t('pages.pokemon.loadingEdit')">
<section v-else class="modal-edit-form skeleton-detail-section" aria-busy="true" :aria-label="t('pages.pokemon.loadingEdit')">
<div v-for="index in 5" :key="index" class="field">
<Skeleton :width="index === 1 ? '52px' : '88px'" />
<Skeleton variant="box" height="44px" />
</div>
</section>
</section>
<template v-if="!loading && options" #footer>
<button type="submit" form="pokemon-edit-form" class="link-button" :disabled="busy">{{ busy ? t('common.saving') : t('common.save') }}</button>
<button type="button" class="plain-button" :disabled="busy" @click="closeEditor">{{ t('common.cancel') }}</button>
</template>
</Modal>
</template>