From 3d99f00c75f8a846c2d2c0aadbc478cee9f3ee00 Mon Sep 17 00:00:00 2001 From: xiaomai Date: Sun, 3 May 2026 10:11:04 +0800 Subject: [PATCH] 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. --- DESIGN.md | 8 +- backend/db/schema.sql | 12 ++ backend/src/queries.ts | 160 ++++++++++++++----- frontend/src/components/EditHistoryPanel.vue | 2 + frontend/src/services/api.ts | 15 +- frontend/src/views/AdminView.vue | 4 +- frontend/src/views/HabitatEdit.vue | 9 +- frontend/src/views/ItemDetail.vue | 2 +- frontend/src/views/ItemEdit.vue | 4 + frontend/src/views/PokemonDetail.vue | 4 +- frontend/src/views/PokemonEdit.vue | 17 +- frontend/src/views/PokemonList.vue | 2 +- system-wordings.ts | 10 ++ 13 files changed, 191 insertions(+), 58 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index f6d9e97..d54b905 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -246,7 +246,9 @@ Pokemon 可配置: -- ID +- 内部 ID:`id`,系统唯一,用于路由、外键和实体关联;普通 Pokemon 新建时优先与展示 ID 一致,活动 Pokemon 由系统分配唯一内部 ID +- 展示 ID:`display_id`,详情页、列表卡片和选择器中显示为 `#ID` +- 是否为活动物品:`is_event_item` - 名称 - Genus:可为空,支持翻译 - 介绍 / Details:可为空,支持翻译 @@ -269,6 +271,8 @@ Pokemon 可配置: - 翻译 - 排序 +Pokemon 的展示 ID 在普通 Pokemon 和活动 Pokemon 之间可以重复,例如允许同时存在普通 `#1 妙蛙种子` 和活动 `#1 毽子草`。数据库只要求同一个 `display_id + is_event_item` 组合唯一;前端路由和实体关联必须继续使用内部 `id`,不能使用展示 ID 作为路由或外键。 + Pokemon 编辑表单使用标签页组织字段: - 编辑表单提供 Fetch data 功能: @@ -341,6 +345,7 @@ Pokemon 详情页展示: 物品可配置: - 名称 +- 是否为活动物品:`is_event_item` - 分类:必填 - 用途:可为空 - 入手方式:可多选 @@ -423,6 +428,7 @@ Pokemon 详情页展示: 栖息地可配置: - 名称 +- 是否为活动物品:`is_event_item` - 配方:多项物品 + 数量 - 可出现的 Pokemon - 图片:通过通用 Wiki 图片上传维护当前图片和历史上传记录 diff --git a/backend/db/schema.sql b/backend/db/schema.sql index c5f37cc..ddc6501 100644 --- a/backend/db/schema.sql +++ b/backend/db/schema.sql @@ -263,7 +263,9 @@ CREATE TABLE IF NOT EXISTS pokemon_types ( CREATE TABLE IF NOT EXISTS pokemon ( id integer PRIMARY KEY, + display_id integer NOT NULL CHECK (display_id > 0), name text NOT NULL UNIQUE, + is_event_item boolean NOT NULL DEFAULT false, genus text NOT NULL DEFAULT '', details text NOT NULL DEFAULT '', height_inches double precision NOT NULL DEFAULT 0 CHECK (height_inches >= 0), @@ -330,12 +332,14 @@ CREATE TABLE IF NOT EXISTS items ( dual_dyeable boolean NOT NULL DEFAULT false, pattern_editable boolean NOT NULL DEFAULT false, no_recipe boolean NOT NULL DEFAULT false, + is_event_item boolean NOT NULL DEFAULT false, image_path text NOT NULL DEFAULT '', sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); ALTER TABLE items ALTER COLUMN usage_id DROP NOT NULL; ALTER TABLE items ADD COLUMN IF NOT EXISTS no_recipe boolean NOT NULL DEFAULT false; +ALTER TABLE items ADD COLUMN IF NOT EXISTS is_event_item boolean NOT NULL DEFAULT false; ALTER TABLE items ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT ''; ALTER TABLE items DROP COLUMN IF EXISTS no_habitat; @@ -422,6 +426,7 @@ CREATE TABLE IF NOT EXISTS maps ( CREATE TABLE IF NOT EXISTS habitats ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE, + is_event_item boolean NOT NULL DEFAULT false, image_path text NOT NULL DEFAULT '', sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0) ); @@ -472,6 +477,10 @@ ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENC ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); +ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS display_id integer; +UPDATE pokemon SET display_id = id WHERE display_id IS NULL; +ALTER TABLE pokemon ALTER COLUMN display_id SET NOT NULL; +ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS is_event_item boolean NOT NULL DEFAULT false; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS genus text NOT NULL DEFAULT ''; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS details text NOT NULL DEFAULT ''; ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS height_inches double precision NOT NULL DEFAULT 0 CHECK (height_inches >= 0); @@ -517,6 +526,7 @@ ALTER TABLE items ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES ALTER TABLE items ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE items ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE items ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); +ALTER TABLE items ADD COLUMN IF NOT EXISTS is_event_item boolean NOT NULL DEFAULT false; ALTER TABLE recipes ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE recipes ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL; @@ -535,6 +545,7 @@ ALTER TABLE habitats ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFEREN ALTER TABLE habitats ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE habitats ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE habitats ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0); +ALTER TABLE habitats ADD COLUMN IF NOT EXISTS is_event_item boolean NOT NULL DEFAULT false; ALTER TABLE habitats ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT ''; WITH ordered AS ( @@ -672,6 +683,7 @@ CREATE INDEX IF NOT EXISTS skills_sort_order_idx ON skills(sort_order, id); CREATE INDEX IF NOT EXISTS favorite_things_sort_order_idx ON favorite_things(sort_order, id); CREATE INDEX IF NOT EXISTS pokemon_types_sort_order_idx ON pokemon_types(sort_order, id); CREATE INDEX IF NOT EXISTS pokemon_sort_order_idx ON pokemon(sort_order, id); +CREATE UNIQUE INDEX IF NOT EXISTS pokemon_display_event_item_key ON pokemon(display_id, is_event_item); CREATE INDEX IF NOT EXISTS life_tags_sort_order_idx ON life_tags(sort_order, id); CREATE INDEX IF NOT EXISTS item_categories_sort_order_idx ON item_categories(sort_order, id); CREATE INDEX IF NOT EXISTS item_usages_sort_order_idx ON item_usages(sort_order, id); diff --git a/backend/src/queries.ts b/backend/src/queries.ts index e45983d..d720838 100644 --- a/backend/src/queries.ts +++ b/backend/src/queries.ts @@ -101,7 +101,8 @@ type PokemonImageOptionsResult = { }; type PokemonPayload = { - id: number; + displayId: number; + isEventItem: boolean; name: string; genus: string; details: string; @@ -154,6 +155,7 @@ type ItemPayload = { dualDyeable: boolean; patternEditable: boolean; noRecipe: boolean; + isEventItem: boolean; acquisitionMethodIds: number[]; tagIds: number[]; imagePath: string; @@ -250,6 +252,7 @@ type LifePostsPage = { type HabitatPayload = { name: string; translations: TranslationInput; + isEventItem: boolean; imagePath: string; recipeItems: IdQuantity[]; pokemonAppearances: Array<{ @@ -283,6 +286,8 @@ type EditHistoryEntry = { user: { id: number; displayName: string } | null; }; type PokemonChangeSource = { + displayId: number; + isEventItem: boolean; name: string; genus: string; details: string; @@ -297,6 +302,7 @@ type PokemonChangeSource = { }; type ItemChangeSource = { name: string; + isEventItem: boolean; image: EntityImageValue | null; category: { name: string }; usage: { name: string } | null; @@ -307,6 +313,7 @@ type ItemChangeSource = { }; type HabitatChangeSource = { name: string; + isEventItem: boolean; image: EntityImageValue | null; recipe: Array<{ name: string; quantity: number }>; pokemon: Array<{ name: string; time_of_day: string; weather: string; rarity: number; map: { name: string } }>; @@ -712,6 +719,30 @@ async function nextSortOrder(client: DbClient, tableName: string): Promise { + if (isEventItem) { + const result = await client.query<{ id: number }>( + 'SELECT COALESCE(MAX(id), 999999) + 1 AS id FROM pokemon WHERE id >= 1000000' + ); + const nextId = result.rows[0]?.id ?? 1000000; + return nextId === displayId ? nextId + 1 : nextId; + } + + if (!isEventItem) { + const preferredId = await client.query<{ id: number }>('SELECT id FROM pokemon WHERE id = $1', [displayId]); + if (preferredId.rowCount === 0) { + return displayId; + } + } + + const result = await client.query<{ id: number }>( + 'SELECT COALESCE(MAX(id), 0) + 1 AS id FROM pokemon WHERE id <> $1', + [displayId] + ); + const nextId = result.rows[0]?.id ?? 1; + return nextId === displayId ? nextId + 1 : nextId; +} + async function reorderTableRows( client: DbClient, tableName: string, @@ -1717,6 +1748,8 @@ async function pokemonEditChanges( .join(' / '); pushChange(changes, 'Name', before.name, after.name); + pushChange(changes, 'Pokemon ID', String(before.displayId), String(after.displayId)); + pushChange(changes, 'Event item', boolValue(before.isEventItem), boolValue(after.isEventItem)); pushChange(changes, 'Genus', before.genus, after.genus); pushChange(changes, 'Details', before.details, after.details); pushChange(changes, 'Height', pokemonHeightValue(before.heightInches), pokemonHeightValue(after.heightInches)); @@ -1744,6 +1777,7 @@ async function itemEditChanges( const tagNames = await entityNameMap(client, 'favorite_things', after.tagIds); pushChange(changes, 'Name', before.name, after.name); + pushChange(changes, 'Event item', boolValue(before.isEventItem), boolValue(after.isEventItem)); pushChange(changes, 'Image', imagePathLabel(before.image?.path), imagePathLabel(after.imagePath)); pushChange(changes, 'Category', before.category.name, categoryNames.get(after.categoryId)); pushChange(changes, 'Usage', before.usage?.name, after.usageId ? usageNames.get(after.usageId) : null); @@ -1776,6 +1810,7 @@ async function habitatEditChanges( .join(' / '); pushChange(changes, 'Name', before.name, after.name); + pushChange(changes, 'Event item', boolValue(before.isEventItem), boolValue(after.isEventItem)); pushChange(changes, 'Image', imagePathLabel(before.image?.path), imagePathLabel(after.imagePath)); pushChange(changes, 'Recipe', quantityListValue(before.recipe), await quantityPayloadValue(client, after.recipeItems)); pushChange(changes, 'Possible Pokemon', appearanceListValue(before.pokemon), afterAppearances); @@ -1832,8 +1867,10 @@ function pokemonProjection(locale: string): string { return ` SELECT p.id, + p.display_id AS "displayId", ${pokemonName} AS name, p.name AS "baseName", + p.is_event_item AS "isEventItem", ${pokemonGenus} AS genus, p.genus AS "baseGenus", ${pokemonDetails} AS details, @@ -3117,7 +3154,9 @@ export async function getPokemon(id: number, locale = defaultLocale) { ) SELECT related_pokemon.id, + related_pokemon.display_id AS "displayId", ${relatedPokemonName} AS name, + related_pokemon.is_event_item AS "isEventItem", ${pokemonImageJson('related_pokemon')} AS image, json_build_object('id', related_environment.id, 'name', ${relatedEnvironmentName}) AS environment, COALESCE(( @@ -3215,10 +3254,11 @@ function cleanPokemonPayload(payload: Record): PokemonPayload { } } - const id = requirePositiveInteger(payload.id, 'Pokemon ID is required'); + const displayId = requirePositiveInteger(payload.displayId ?? payload.id, 'Pokemon ID is required'); return { - id, + displayId, + isEventItem: Boolean(payload.isEventItem), name: cleanName(payload.name, 'Pokemon name is required'), genus: cleanOptionalText(payload.genus), details: cleanOptionalText(payload.details), @@ -3231,7 +3271,7 @@ function cleanPokemonPayload(payload: Record): PokemonPayload { skillIds, favoriteThingIds, skillItemDrops: [...skillItemDrops.values()], - image: cleanPokemonImage(payload.imagePath, id) + image: cleanPokemonImage(payload.imagePath, displayId) }; } @@ -3284,12 +3324,15 @@ export async function createPokemon(payload: Record, userId: nu const cleanPayload = cleanPokemonPayload(payload); const id = await withTransaction(async (client) => { + const pokemonId = await nextPokemonInternalId(client, cleanPayload.displayId, cleanPayload.isEventItem); const sortOrder = await nextSortOrder(client, 'pokemon'); await client.query( ` INSERT INTO pokemon ( id, + display_id, name, + is_event_item, genus, details, height_inches, @@ -3310,11 +3353,13 @@ export async function createPokemon(payload: Record, userId: nu created_by_user_id, updated_by_user_id ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $20) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $22) `, [ - cleanPayload.id, + pokemonId, + cleanPayload.displayId, cleanPayload.name, + cleanPayload.isEventItem, cleanPayload.genus, cleanPayload.details, cleanPayload.heightInches, @@ -3335,17 +3380,17 @@ export async function createPokemon(payload: Record, userId: nu userId ] ); - await linkEntityImageUpload(client, 'pokemon', cleanPayload.id, cleanPayload.image?.path, cleanPayload.name); - await replacePokemonRelations(client, cleanPayload.id, cleanPayload); - await replaceEntityTranslations(client, 'pokemon', cleanPayload.id, cleanPayload.translations, ['name', 'details', 'genus']); - await recordEditLog(client, 'pokemon', cleanPayload.id, 'create', userId); - return cleanPayload.id; + await linkEntityImageUpload(client, 'pokemon', pokemonId, cleanPayload.image?.path, cleanPayload.name); + await replacePokemonRelations(client, pokemonId, cleanPayload); + await replaceEntityTranslations(client, 'pokemon', pokemonId, cleanPayload.translations, ['name', 'details', 'genus']); + await recordEditLog(client, 'pokemon', pokemonId, 'create', userId); + return pokemonId; }); return getPokemon(id, locale); } export async function updatePokemon(id: number, payload: Record, userId: number, locale = defaultLocale) { - const cleanPayload = cleanPokemonPayload({ ...payload, id }); + const cleanPayload = cleanPokemonPayload(payload); const before = await getPokemon(id, defaultLocale); const updated = await withTransaction(async (client) => { @@ -3353,29 +3398,33 @@ export async function updatePokemon(id: number, payload: Record ` UPDATE pokemon SET - name = $1, - genus = $2, - details = $3, - height_inches = $4, - weight_pounds = $5, - environment_id = $6, - hp = $7, - attack = $8, - defense = $9, - special_attack = $10, - special_defense = $11, - speed = $12, - image_path = $13, - image_style = $14, - image_version = $15, - image_variant = $16, - image_description = $17, - updated_by_user_id = $18, + display_id = $1, + name = $2, + is_event_item = $3, + genus = $4, + details = $5, + height_inches = $6, + weight_pounds = $7, + environment_id = $8, + hp = $9, + attack = $10, + defense = $11, + special_attack = $12, + special_defense = $13, + speed = $14, + image_path = $15, + image_style = $16, + image_version = $17, + image_variant = $18, + image_description = $19, + updated_by_user_id = $20, updated_at = now() - WHERE id = $19 + WHERE id = $21 `, [ + cleanPayload.displayId, cleanPayload.name, + cleanPayload.isEventItem, cleanPayload.genus, cleanPayload.details, cleanPayload.heightInches, @@ -3433,6 +3482,7 @@ export async function listHabitats(locale = defaultLocale) { h.id, ${habitatName} AS name, h.name AS "baseName", + h.is_event_item AS "isEventItem", ${translationsSelect('habitats', 'h.id')} AS translations, ${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')}, ${uploadedImageJson('h.image_path')} AS image, @@ -3443,9 +3493,17 @@ export async function listHabitats(locale = defaultLocale) { WHERE hri.habitat_id = h.id ), '[]'::json) AS recipe, COALESCE(( - SELECT json_agg(json_build_object('id', pokemon_rows.id, 'name', pokemon_rows.name) ORDER BY pokemon_rows.sort_order, pokemon_rows.id) + SELECT json_agg( + json_build_object( + 'id', pokemon_rows.id, + 'displayId', pokemon_rows.display_id, + 'name', pokemon_rows.name, + 'isEventItem', pokemon_rows.is_event_item + ) + ORDER BY pokemon_rows.sort_order, pokemon_rows.id + ) FROM ( - SELECT DISTINCT p.id, ${pokemonName} AS name, p.sort_order + SELECT DISTINCT p.id, p.display_id, ${pokemonName} AS name, p.is_event_item, p.sort_order FROM habitat_pokemon hp JOIN pokemon p ON p.id = hp.pokemon_id WHERE hp.habitat_id = h.id @@ -3469,6 +3527,7 @@ export async function getHabitat(id: number, locale = defaultLocale) { h.id, ${habitatName} AS name, h.name AS "baseName", + h.is_event_item AS "isEventItem", ${translationsSelect('habitats', 'h.id')} AS translations, ${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')}, ${uploadedImageJson('h.image_path')} AS image, @@ -3502,7 +3561,9 @@ export async function getHabitat(id: number, locale = defaultLocale) { ` SELECT p.id, + p.display_id AS "displayId", ${pokemonName} AS name, + p.is_event_item AS "isEventItem", ${pokemonImageJson('p')} AS image, hp.time_of_day, hp.weather, @@ -3557,6 +3618,7 @@ function cleanHabitatPayload(payload: Record): HabitatPayload { return { name: cleanName(payload.name, 'Habitat name is required'), translations: cleanTranslations(payload.translations, ['name']), + isEventItem: Boolean(payload.isEventItem), imagePath: cleanUploadImagePath(payload.imagePath, 'habitats'), recipeItems: cleanQuantities(payload.recipeItems), pokemonAppearances: [...pokemonAppearances.values()] @@ -3593,11 +3655,11 @@ export async function createHabitat(payload: Record, userId: nu const sortOrder = await nextSortOrder(client, 'habitats'); const result = await client.query<{ id: number }>( ` - INSERT INTO habitats (name, image_path, sort_order, created_by_user_id, updated_by_user_id) - VALUES ($1, $2, $3, $4, $4) + INSERT INTO habitats (name, is_event_item, image_path, sort_order, created_by_user_id, updated_by_user_id) + VALUES ($1, $2, $3, $4, $5, $5) RETURNING id `, - [cleanPayload.name, cleanPayload.imagePath, sortOrder, userId] + [cleanPayload.name, cleanPayload.isEventItem, cleanPayload.imagePath, sortOrder, userId] ); const habitatId = result.rows[0].id; await linkEntityImageUpload(client, 'habitats', habitatId, cleanPayload.imagePath, cleanPayload.name); @@ -3615,8 +3677,8 @@ export async function updateHabitat(id: number, payload: Record const updated = await withTransaction(async (client) => { const result = await client.query( - 'UPDATE habitats SET name = $1, image_path = $2, updated_by_user_id = $3, updated_at = now() WHERE id = $4', - [cleanPayload.name, cleanPayload.imagePath, userId, id] + 'UPDATE habitats SET name = $1, is_event_item = $2, image_path = $3, updated_by_user_id = $4, updated_at = now() WHERE id = $5', + [cleanPayload.name, cleanPayload.isEventItem, cleanPayload.imagePath, userId, id] ); if (result.rowCount === 0) { return false; @@ -3656,6 +3718,7 @@ function itemProjection(locale: string): string { i.id, ${itemName} AS name, i.name AS "baseName", + i.is_event_item AS "isEventItem", ${translationsSelect('items', 'i.id')} AS translations, ${auditSelect('i', 'item_created_user', 'item_updated_user')}, ${uploadedImageJson('i.image_path')} AS image, @@ -3873,7 +3936,13 @@ export async function getItem(id: number, locale = defaultLocale) { query( ` SELECT - json_build_object('id', p.id, 'name', ${pokemonName}, 'image', ${pokemonImageJson('p')}) AS pokemon, + json_build_object( + 'id', p.id, + 'displayId', p.display_id, + 'name', ${pokemonName}, + 'isEventItem', p.is_event_item, + 'image', ${pokemonImageJson('p')} + ) AS pokemon, json_build_object('id', s.id, 'name', ${skillName}) AS skill FROM pokemon_skill_item_drops psid JOIN pokemon p ON p.id = psid.pokemon_id @@ -3905,6 +3974,7 @@ function cleanItemPayload(payload: Record): ItemPayload { dualDyeable: Boolean(payload.dualDyeable), patternEditable: Boolean(payload.patternEditable), noRecipe: Boolean(payload.noRecipe), + isEventItem: Boolean(payload.isEventItem), acquisitionMethodIds: cleanIds(payload.acquisitionMethodIds), tagIds: cleanIds(payload.tagIds), imagePath: cleanUploadImagePath(payload.imagePath, 'items') @@ -3956,12 +4026,13 @@ export async function createItem(payload: Record, userId: numbe dual_dyeable, pattern_editable, no_recipe, + is_event_item, image_path, sort_order, created_by_user_id, updated_by_user_id ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $10) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11) RETURNING id `, [ @@ -3972,6 +4043,7 @@ export async function createItem(payload: Record, userId: numbe cleanPayload.dualDyeable, cleanPayload.patternEditable, cleanPayload.noRecipe, + cleanPayload.isEventItem, cleanPayload.imagePath, sortOrder, userId @@ -4003,10 +4075,11 @@ export async function updateItem(id: number, payload: Record, u dual_dyeable = $5, pattern_editable = $6, no_recipe = $7, - image_path = $8, - updated_by_user_id = $9, + is_event_item = $8, + image_path = $9, + updated_by_user_id = $10, updated_at = now() - WHERE id = $10 + WHERE id = $11 `, [ cleanPayload.name, @@ -4016,6 +4089,7 @@ export async function updateItem(id: number, payload: Record, u cleanPayload.dualDyeable, cleanPayload.patternEditable, cleanPayload.noRecipe, + cleanPayload.isEventItem, cleanPayload.imagePath, userId, id diff --git a/frontend/src/components/EditHistoryPanel.vue b/frontend/src/components/EditHistoryPanel.vue index 48819dc..3238175 100644 --- a/frontend/src/components/EditHistoryPanel.vue +++ b/frontend/src/components/EditHistoryPanel.vue @@ -12,6 +12,8 @@ const changeLabelKeys: Record = { 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', diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index b862ef0..d96d4f4 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -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; @@ -165,6 +170,8 @@ export interface HabitatDetail extends Habitat { editHistory: EditHistoryEntry[]; imageHistory: EntityImageUpload[]; pokemon: Array; } @@ -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<{ diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index 7bcbe3e..2b4ab0b 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -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" >