feat(wiki): add event item flag and decouple pokemon display ID
Add `is_event_item` to pokemon, items, and habitats. Separate internal `id` and `display_id` for pokemon to allow event variants. Update frontend forms and views to support the new fields.
This commit is contained in:
@@ -12,6 +12,8 @@ const changeLabelKeys: Record<string, string> = {
|
||||
Name: 'common.name',
|
||||
名字: 'common.name',
|
||||
名称: 'common.name',
|
||||
'Pokemon ID': 'pages.pokemon.id',
|
||||
'Event item': 'common.eventItem',
|
||||
Genus: 'pages.pokemon.genus',
|
||||
Details: 'pages.pokemon.details',
|
||||
介绍: 'pages.pokemon.details',
|
||||
|
||||
@@ -106,8 +106,10 @@ export interface EditHistoryEntry {
|
||||
|
||||
export interface Pokemon extends EditInfo {
|
||||
id: number;
|
||||
displayId: number;
|
||||
name: string;
|
||||
baseName?: string;
|
||||
isEventItem: boolean;
|
||||
genus: string;
|
||||
baseGenus?: string;
|
||||
details: string;
|
||||
@@ -127,7 +129,9 @@ export interface Pokemon extends EditInfo {
|
||||
|
||||
export interface RelatedPokemon {
|
||||
id: number;
|
||||
displayId: number;
|
||||
name: string;
|
||||
isEventItem: boolean;
|
||||
image?: PokemonImage | null;
|
||||
environment: NamedEntity;
|
||||
skills: Skill[];
|
||||
@@ -155,6 +159,7 @@ export interface Habitat extends EditInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
baseName?: string;
|
||||
isEventItem: boolean;
|
||||
translations?: TranslationMap;
|
||||
image: EntityImage | null;
|
||||
recipe: Array<NamedEntity & { image?: EntityImage | null; quantity: number }>;
|
||||
@@ -165,6 +170,8 @@ export interface HabitatDetail extends Habitat {
|
||||
editHistory: EditHistoryEntry[];
|
||||
imageHistory: EntityImageUpload[];
|
||||
pokemon: Array<NamedEntity & {
|
||||
displayId: number;
|
||||
isEventItem: boolean;
|
||||
image?: PokemonImage | null;
|
||||
time_of_day: string;
|
||||
weather: string;
|
||||
@@ -201,6 +208,7 @@ export interface Item extends EditInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
baseName?: string;
|
||||
isEventItem: boolean;
|
||||
translations?: TranslationMap;
|
||||
image: EntityImage | null;
|
||||
category: NamedEntity;
|
||||
@@ -223,7 +231,7 @@ export interface ItemDetail extends Item {
|
||||
editHistory: EditHistoryEntry[];
|
||||
imageHistory: EntityImageUpload[];
|
||||
droppedByPokemon: Array<{
|
||||
pokemon: NamedEntity & { image?: PokemonImage | null };
|
||||
pokemon: NamedEntity & { displayId: number; isEventItem: boolean; image?: PokemonImage | null };
|
||||
skill: NamedEntity;
|
||||
}>;
|
||||
}
|
||||
@@ -339,7 +347,8 @@ export type ConfigType =
|
||||
| 'life-tags';
|
||||
|
||||
export interface PokemonPayload {
|
||||
id: number;
|
||||
displayId: number;
|
||||
isEventItem: boolean;
|
||||
name: string;
|
||||
genus: string;
|
||||
details: string;
|
||||
@@ -388,6 +397,7 @@ export interface ItemPayload {
|
||||
dualDyeable: boolean;
|
||||
patternEditable: boolean;
|
||||
noRecipe: boolean;
|
||||
isEventItem: boolean;
|
||||
acquisitionMethodIds: number[];
|
||||
tagIds: number[];
|
||||
imagePath: string;
|
||||
@@ -402,6 +412,7 @@ export interface RecipePayload {
|
||||
export interface HabitatPayload {
|
||||
name: string;
|
||||
translations?: TranslationMap;
|
||||
isEventItem: boolean;
|
||||
imagePath: string;
|
||||
recipeItems: Array<{ itemId: number; quantity: number }>;
|
||||
pokemonAppearances: Array<{
|
||||
|
||||
@@ -188,7 +188,7 @@ const languageLabel = (item: Language) => item.name;
|
||||
const configKey = (item: EditableConfig) => item.id;
|
||||
const configLabel = (item: EditableConfig) => item.name;
|
||||
const pokemonKey = (item: Pokemon) => item.id;
|
||||
const pokemonLabel = (item: Pokemon) => `#${item.id} ${item.name}`;
|
||||
const pokemonLabel = (item: Pokemon) => `#${item.displayId} ${item.name}`;
|
||||
const itemKey = (item: Item) => item.id;
|
||||
const itemLabel = (item: Item) => item.name;
|
||||
const recipeKey = (item: Recipe) => item.id;
|
||||
@@ -921,7 +921,7 @@ onMounted(() => {
|
||||
@reorder="persistPokemonOrder"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<RouterLink :to="`/pokemon/${item.id}`">#{{ item.id }} {{ item.name }}</RouterLink>
|
||||
<RouterLink :to="`/pokemon/${item.id}`">#{{ item.displayId }} {{ item.name }}</RouterLink>
|
||||
<span class="row-actions">
|
||||
<button type="button" :disabled="busy" @click="removePokemon(item.id)">
|
||||
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||
|
||||
@@ -49,6 +49,7 @@ const creatingSelect = ref('');
|
||||
const habitatForm = ref({
|
||||
name: '',
|
||||
translations: {} as TranslationMap,
|
||||
isEventItem: false,
|
||||
imagePath: '',
|
||||
recipeItems: [] as Array<{ itemId: string; quantity: number }>,
|
||||
pokemonAppearances: [] as HabitatAppearanceForm[]
|
||||
@@ -71,7 +72,7 @@ const routeId = computed(() => (typeof route.params.id === 'string' ? route.para
|
||||
const isEditing = computed(() => routeId.value !== '');
|
||||
const itemSelectOptions = computed(() => itemRows.value.map((item) => ({ id: item.id, name: item.name })));
|
||||
const pokemonSelectOptions = computed(() =>
|
||||
pokemonRows.value.map((pokemon) => ({ id: pokemon.id, name: pokemon.name, label: `#${pokemon.id} ${pokemon.name}` }))
|
||||
pokemonRows.value.map((pokemon) => ({ id: pokemon.id, name: pokemon.name, label: `#${pokemon.displayId} ${pokemon.name}` }))
|
||||
);
|
||||
const pageTitle = computed(() =>
|
||||
isEditing.value
|
||||
@@ -166,6 +167,7 @@ async function loadEditor() {
|
||||
habitatForm.value = {
|
||||
name: habitat.baseName ?? habitat.name,
|
||||
translations: habitat.translations ?? {},
|
||||
isEventItem: habitat.isEventItem,
|
||||
imagePath: habitat.image?.path ?? '',
|
||||
recipeItems: habitat.recipe.map((recipeItem) => ({ itemId: String(recipeItem.id), quantity: recipeItem.quantity })),
|
||||
pokemonAppearances: groupPokemonAppearances(habitat)
|
||||
@@ -212,6 +214,7 @@ async function saveHabitat() {
|
||||
const payload: HabitatPayload = {
|
||||
name: habitatNameForSave(),
|
||||
translations: habitatForm.value.translations,
|
||||
isEventItem: habitatForm.value.isEventItem,
|
||||
imagePath: habitatForm.value.imagePath,
|
||||
recipeItems: toQuantityRows(habitatForm.value.recipeItems),
|
||||
pokemonAppearances: habitatForm.value.pokemonAppearances
|
||||
@@ -276,6 +279,10 @@ onMounted(() => {
|
||||
@error="message = $event"
|
||||
/>
|
||||
|
||||
<div class="check-row">
|
||||
<label><input v-model="habitatForm.isEventItem" type="checkbox" /> {{ t('pages.habitats.eventItem') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('pages.habitats.recipe') }}</label>
|
||||
<div v-for="(row, index) in habitatForm.recipeItems" :key="index" class="inline-row">
|
||||
|
||||
@@ -258,7 +258,7 @@ watch(
|
||||
<img v-if="entry.pokemon.image" :src="entry.pokemon.image.url" alt="" loading="lazy" />
|
||||
<PokeBallMark v-else size="22px" />
|
||||
</span>
|
||||
<span>#{{ entry.pokemon.id }} {{ entry.pokemon.name }}</span>
|
||||
<span>#{{ entry.pokemon.displayId }} {{ entry.pokemon.name }}</span>
|
||||
</RouterLink>
|
||||
<span>{{ t('pages.pokemon.skillDrop', { name: entry.skill.name }) }}</span>
|
||||
</li>
|
||||
|
||||
@@ -32,6 +32,7 @@ const itemForm = ref({
|
||||
dualDyeable: false,
|
||||
patternEditable: false,
|
||||
noRecipe: false,
|
||||
isEventItem: false,
|
||||
acquisitionMethodIds: [] as string[],
|
||||
tagIds: [] as string[],
|
||||
imagePath: ''
|
||||
@@ -92,6 +93,7 @@ async function loadEditor() {
|
||||
dualDyeable: item.customization.dualDyeable,
|
||||
patternEditable: item.customization.patternEditable,
|
||||
noRecipe: item.noRecipe,
|
||||
isEventItem: item.isEventItem,
|
||||
acquisitionMethodIds: item.acquisitionMethods.map((method) => String(method.id)),
|
||||
tagIds: item.tags.map((tag) => String(tag.id)),
|
||||
imagePath: item.image?.path ?? ''
|
||||
@@ -158,6 +160,7 @@ async function saveItem() {
|
||||
dualDyeable: itemForm.value.dualDyeable,
|
||||
patternEditable: itemForm.value.patternEditable,
|
||||
noRecipe: itemForm.value.noRecipe,
|
||||
isEventItem: itemForm.value.isEventItem,
|
||||
acquisitionMethodIds: toIds(itemForm.value.acquisitionMethodIds),
|
||||
tagIds: toIds(itemForm.value.tagIds),
|
||||
imagePath: itemForm.value.imagePath
|
||||
@@ -249,6 +252,7 @@ onMounted(() => {
|
||||
<label><input v-model="itemForm.dualDyeable" type="checkbox" /> {{ t('pages.items.dualDyeable') }}</label>
|
||||
<label><input v-model="itemForm.patternEditable" type="checkbox" /> {{ t('pages.items.patternEditable') }}</label>
|
||||
<label><input v-model="itemForm.noRecipe" type="checkbox" :disabled="hasRecipe" /> {{ t('pages.items.noRecipe') }}</label>
|
||||
<label><input v-model="itemForm.isEventItem" type="checkbox" /> {{ t('pages.items.eventItem') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
|
||||
@@ -304,7 +304,7 @@ watch(
|
||||
</div>
|
||||
</section>
|
||||
<section v-else class="page-stack">
|
||||
<PageHeader :title="`#${pokemon.id} ${pokemon.name}`" :subtitle="t('pages.pokemon.environmentPrefix', { name: pokemon.environment.name })">
|
||||
<PageHeader :title="`#${pokemon.displayId} ${pokemon.name}`" :subtitle="t('pages.pokemon.environmentPrefix', { name: pokemon.environment.name })">
|
||||
<template #kicker>Pokédex Detail</template>
|
||||
<template #actions>
|
||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/pokemon/${pokemon.id}/edit`">
|
||||
@@ -421,7 +421,7 @@ watch(
|
||||
</span>
|
||||
<div class="related-pokemon-row">
|
||||
<div class="related-pokemon-row__summary">
|
||||
<RouterLink class="related-pokemon-row__name" :to="`/pokemon/${related.id}`">#{{ related.id }} {{ related.name }}</RouterLink>
|
||||
<RouterLink class="related-pokemon-row__name" :to="`/pokemon/${related.id}`">#{{ related.displayId }} {{ related.name }}</RouterLink>
|
||||
<div class="related-pokemon-row__traits">
|
||||
<EntityChips
|
||||
v-if="related.skills.length"
|
||||
|
||||
@@ -70,6 +70,7 @@ function defaultPokemonStats(): PokemonStats {
|
||||
|
||||
const pokemonForm = ref({
|
||||
id: '',
|
||||
isEventItem: false,
|
||||
name: '',
|
||||
genus: '',
|
||||
details: '',
|
||||
@@ -206,7 +207,7 @@ function pokemonNameForSave() {
|
||||
}
|
||||
|
||||
function pokemonIdForSave() {
|
||||
return Number(isEditing.value ? routeId.value : pokemonForm.value.id);
|
||||
return Number(pokemonForm.value.id);
|
||||
}
|
||||
|
||||
function mergeFetchedTranslations(fetchedTranslations: TranslationMap | undefined): TranslationMap {
|
||||
@@ -273,7 +274,8 @@ async function loadEditor() {
|
||||
if (isEditing.value) {
|
||||
const pokemon = await api.pokemonDetail(routeId.value);
|
||||
pokemonForm.value = {
|
||||
id: String(pokemon.id),
|
||||
id: String(pokemon.displayId),
|
||||
isEventItem: pokemon.isEventItem,
|
||||
name: pokemon.baseName ?? pokemon.name,
|
||||
genus: pokemon.baseGenus ?? pokemon.genus,
|
||||
details: pokemon.baseDetails ?? pokemon.details,
|
||||
@@ -535,7 +537,8 @@ async function savePokemon() {
|
||||
|
||||
try {
|
||||
const payload: PokemonPayload = {
|
||||
id: pokemonIdForSave(),
|
||||
displayId: pokemonIdForSave(),
|
||||
isEventItem: pokemonForm.value.isEventItem,
|
||||
name: pokemonNameForSave(),
|
||||
genus: pokemonForm.value.genus,
|
||||
details: pokemonForm.value.details,
|
||||
@@ -630,8 +633,8 @@ watch(fetchIdentifier, refreshFetchOptions);
|
||||
<section v-if="activeEditTab === 'basic'" class="pokemon-edit-panel" role="tabpanel" :aria-label="t('pages.pokemon.editTabBasic')">
|
||||
<div class="pokemon-edit-grid">
|
||||
<div class="field">
|
||||
<label for="pokemon-id">ID</label>
|
||||
<input id="pokemon-id" v-model="pokemonForm.id" :disabled="isEditing" min="1" required type="number" />
|
||||
<label for="pokemon-id">{{ t('pages.pokemon.id') }}</label>
|
||||
<input id="pokemon-id" v-model="pokemonForm.id" min="1" required type="number" />
|
||||
</div>
|
||||
|
||||
<TranslationFields
|
||||
@@ -645,6 +648,10 @@ watch(fetchIdentifier, refreshFetchOptions);
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="check-row">
|
||||
<label><input v-model="pokemonForm.isEventItem" type="checkbox" /> {{ t('pages.pokemon.eventItem') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="pokemon-edit-grid">
|
||||
<div class="field">
|
||||
<label for="pokemon-environment">{{ t('pages.pokemon.environment') }}</label>
|
||||
|
||||
@@ -134,7 +134,7 @@ watch(query, loadPokemon);
|
||||
<EntityCard
|
||||
v-for="item in pokemon"
|
||||
:key="item.id"
|
||||
:title="`#${item.id} ${item.name}`"
|
||||
:title="`#${item.displayId} ${item.name}`"
|
||||
:to="`/pokemon/${item.id}`"
|
||||
:image="pokemonCardImage(item)"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user